Files
nixie-clock/firmware/uniclock/ESP32_UniClock2/VQC10.ino
T
mangavalkandClaude Opus 5.5 b347f1c63f Import ESP_UniClock (upstream 1189810)
Vendored from https://github.com/gpeter62/ESP_UniClock: the sketch
source, the web UI and the libraries the ESP8266 build needs, unpacked
from the bundled zips. Adds a PlatformIO project for the ESP-12. The
source is unchanged; local changes follow as separate commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:48:04 +01:00

626 lines
31 KiB
Arduino
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// VQC10 driver
// ----------------------------------------------------------------------------
// Drives 8x VQC10 tubes/modules, each a 5x7 dot-matrix (LED or VFD-style)
// character display (see Datasheets/VQC10_Funkamateur.pdf and
// "VQC10 timing.jpeg" for the original hardware reference - VQC10 is an
// Eastern-European 5x7 dot-matrix indicator tube, not a classic 7-segment
// Nixie/VFD tube). Because each character cell is a dot matrix rather than
// a fixed set of segments, this driver renders full ASCII text (via the
// bitmap _font[] table below) instead of just digits.
//
// Wiring / protocol: the 8 character cells are driven through a chain of
// 74HC595 serial-in/parallel-out shift registers (latchPin=RCK/STCP,
// clockPin=SCK/SHCP, dataPin=DIN/SER - classic 3-wire SPI-like shift-out,
// no MISO). Unlike most other drivers in this project (which multiplex
// digit-by-digit), the VQC10 hardware is scanned ROW-by-ROW: for every one
// of the NUMROWS=7 dot-rows, the column data for all NUMDIGITS=8 characters
// is shifted out and latched digit-by-digit, then a single row-select line
// is pulsed on for a PWM-timed on-period before moving to the next row.
// Refresh timing is fully cooperative/blocking (see writeDisplay2() below)
// rather than driven from the hardware timer ISR - see the note above
// writeDisplay() for why.
//
// Used by clocks.h configuration(s): CLOCK_71 ("2x VQC10 clock by UNFI").
// ============================================================================
#ifdef VQC10
char tubeDriver[] = "VQC10";
#define NUMDIGITS 8
int maxDigits = NUMDIGITS;
//Please, set the pin numbers in clock.h!
//#define latchPin 5 //D2 RCK
//#define clockPin 4 //D1 SCK
//#define dataPin 2 //D4 DIN
//#define ledOffpin 0 //D5
//#define D5pin 14
#define NUMCOLS 5 // font glyph width in dots (columns per character cell)
#define NUMROWS 7 // font glyph height in dots (rows per character cell) - also the number of physical row-select lines scanned in writeDisplay2()
boolean _upsidedown = false; // flips the dot-matrix rendering 180 degrees (for tubes mounted upside down); never set to true anywhere in this codebase - effectively dead/unused, kept for parity with wordclock.ino which shares this same font/transpose code
byte row[NUMROWS]; // scratch buffer filled by show()/transposePattern(): one row-bitmap per dot-row for the character currently being rendered
// Maps the shared digit[]/newDigit[] buffer codes (see core file: 0-9=digit, 10=BLANK,
// 11=minus, 12=decimal point, 13+=driver-specific extra glyphs) to an index into the
// ASCII-ordered _font[] table below, i.e. this converts the generic "digit code" alphabet
// used by the core firmware into actual character codes this driver can render.
char asciiConvert[] = "0123456789 -.APC~%oIF";
char DPchar = 0xFF; //decimal point - index of the dedicated 1-dot "decimal point" glyph appended at the very end of _font[] (see 0xFF entry below)
char cDigit[NUMDIGITS+1] = "76543210"; // NOTE: declared/initialized but never referenced anywhere else in this file or the project - dead code (candidate for removal)
byte dat[NUMDIGITS][NUMROWS]; // final row-bitmaps actually shifted out to hardware (post-animation-blend result)
byte oldDat[NUMDIGITS][NUMROWS]; // row-bitmaps for the outgoing (old) character, built fresh each writeDisplaySingle() call
byte newDat[NUMDIGITS][NUMROWS]; // row-bitmaps for the incoming (new) character, built fresh each writeDisplaySingle() call
// standard ascii 5x7 font incl. the IBM-PC extension:
// already defined in DotMatrx5x7.h
// Format: NUMCOLS(5) bytes per glyph, indexed as glyph_code*NUMCOLS in show() above; each
// byte is one column of the cell, with dot rows packed in its low NUMROWS(7) bits (bit0=row0
// top .. bit6=row6 bottom in normal orientation). Glyph 0xFF at the very end of the table is
// repurposed as a dedicated "decimal point" glyph (see DPchar above) rather than a real
// character, so it only lights a single dot instead of a printable symbol.
//#define aUML "\x84" // ä = 0x84
//#define oUML "\x94" // ö = 0x94
//#define uUML "\x81" // ü = 0x81
//#define sZET "\xE1" // ß = 0xE1
//#define AUML "\x8E" // Ä = 0x8E
//#define OUML "\x99" // Ö = 0x99
//#define UUML "\x9A" // Ü = 0x9A
byte _font[] = {
0x00, 0x00, 0x00, 0x00, 0x00, // 0x00 empty
0x3E, 0x5B, 0x4F, 0x5B, 0x3E, // 0x01 sad
0x3E, 0x6B, 0x4F, 0x6B, 0x3E, // 0x02 happy
0x1C, 0x3E, 0x7C, 0x3E, 0x1C, // 0x03 heart
0x18, 0x3C, 0x7E, 0x3C, 0x18, // 0x04 diamond
0x1C, 0x57, 0x7D, 0x57, 0x1C, // 0x05 clover
0x1C, 0x5E, 0x7F, 0x5E, 0x1C, // 0x06 spade
0x00, 0x18, 0x3C, 0x18, 0x00, // 0x07 small full circle
0xFF, 0xE7, 0xC3, 0xE7, 0xFF, // 0x08 inverted small circle
0x06, 0x09, 0x06, 0x00, 0x00, // 0x09 small empty circle //old: 0x00, 0x18, 0x24, 0x18, 0x00, // 0x09 small empty circle
0xFF, 0xE7, 0xDB, 0xE7, 0xFF, // 0x0A block (linefeed)
0x30, 0x48, 0x3A, 0x06, 0x0E, // 0x0B male
0x26, 0x29, 0x79, 0x29, 0x26, // 0x0C female
0x40, 0x7F, 0x05, 0x05, 0x07, // 0x0D note (carriage return)
0x40, 0x7F, 0x05, 0x25, 0x3F, // 0x0E two notes
0x5A, 0x3C, 0xE7, 0x3C, 0x5A, // 0x0F sun
0x7F, 0x3E, 0x1C, 0x1C, 0x08, // 0x10 right triangle
0x08, 0x1C, 0x1C, 0x3E, 0x7F, // 0x11 left triangle
0x14, 0x22, 0x7F, 0x22, 0x14, // 0x12 up/down arrow
0x5F, 0x5F, 0x00, 0x5F, 0x5F, // 0x13 double exclamation mark
0x06, 0x09, 0x7F, 0x01, 0x7F, // 0x14 paragraph end sign (pilcrow)
// 0x00, 0x66, 0x89, 0x95, 0x6A, // 0x15 paragraph sign original
0x00, 0x26, 0x49, 0x55, 0x28, // 0x15 paragraph sign
0x60, 0x60, 0x60, 0x60, 0x60, // 0x16 double underline
0x54, 0x62, 0x7F, 0x62, 0x54, // 0x17 up/down arrow with underline
0x08, 0x04, 0x7E, 0x04, 0x08, // 0x18 up arrow
0x10, 0x20, 0x7E, 0x20, 0x10, // 0x19 down arrow
0x08, 0x08, 0x2A, 0x1C, 0x08, // 0x1A right arrow
0x08, 0x1C, 0x2A, 0x08, 0x08, // 0x1B left arrow
0x14, 0x3E, 0x55, 0x41, 0x22, // 0x1C euro
0x63, 0x75, 0x69, 0x75, 0x63, // 0x1D timer
0x30, 0x38, 0x3E, 0x38, 0x30, // 0x1E up triangle
0x06, 0x0E, 0x3E, 0x0E, 0x06, // 0x1F down triangle
0x00, 0x00, 0x00, 0x00, 0x00, // 0x20 space
0x00, 0x00, 0x5F, 0x00, 0x00, // 0x21 !
0x00, 0x07, 0x00, 0x07, 0x00, // 0x22 "
0x14, 0x7F, 0x14, 0x7F, 0x14, // 0x23 #
0x24, 0x2A, 0x7F, 0x2A, 0x12, // 0x24 $
0x23, 0x13, 0x08, 0x64, 0x62, // 0x25 %
0x36, 0x49, 0x56, 0x20, 0x50, // 0x26 &
0x00, 0x08, 0x07, 0x03, 0x00, // 0x27 ´
0x00, 0x1C, 0x22, 0x41, 0x00, // 0x28 (
0x00, 0x41, 0x22, 0x1C, 0x00, // 0x29 )
0x2A, 0x1C, 0x7F, 0x1C, 0x2A, // 0x2A *
0x08, 0x08, 0x3E, 0x08, 0x08, // 0x2B +
0x00, 0x80, 0x70, 0x30, 0x00, // 0x2C ,
0x08, 0x08, 0x08, 0x08, 0x08, // 0x2D -
0x00, 0x00, 0x60, 0x60, 0x00, // 0x2E .
0x20, 0x10, 0x08, 0x04, 0x02, // 0x2F /
0x3E, 0x51, 0x49, 0x45, 0x3E, // 0x30 0
0x00, 0x42, 0x7F, 0x40, 0x00, // 0x31 1
0x72, 0x49, 0x49, 0x49, 0x46, // 0x32 2
0x21, 0x41, 0x49, 0x4D, 0x33, // 0x33 3
0x18, 0x14, 0x12, 0x7F, 0x10, // 0x34 4
0x27, 0x45, 0x45, 0x45, 0x39, // 0x35 5
0x3C, 0x4A, 0x49, 0x49, 0x31, // 0x36 6
0x41, 0x21, 0x11, 0x09, 0x07, // 0x37 7
0x36, 0x49, 0x49, 0x49, 0x36, // 0x38 8
0x46, 0x49, 0x49, 0x29, 0x1E, // 0x39 9
0x00, 0x00, 0x14, 0x00, 0x00, // 0x3A :
0x00, 0x40, 0x34, 0x00, 0x00, // 0x3B ;
0x00, 0x08, 0x14, 0x22, 0x41, // 0x3C <
0x14, 0x14, 0x14, 0x14, 0x14, // 0x3D =
0x00, 0x41, 0x22, 0x14, 0x08, // 0x3E >
0x02, 0x01, 0x59, 0x09, 0x06, // 0x3F ?
0x3E, 0x41, 0x5D, 0x59, 0x4E, // 0x40 @
0x7C, 0x12, 0x11, 0x12, 0x7C, // 0x41 A
0x7F, 0x49, 0x49, 0x49, 0x36, // 0x42 B
0x3E, 0x41, 0x41, 0x41, 0x22, // 0x43 C
0x7F, 0x41, 0x41, 0x41, 0x3E, // 0x44 D
0x7F, 0x49, 0x49, 0x49, 0x41, // 0x45 E
0x7F, 0x09, 0x09, 0x09, 0x01, // 0x46 F
0x3E, 0x41, 0x41, 0x51, 0x73, // 0x47 G
0x7F, 0x08, 0x08, 0x08, 0x7F, // 0x48 H
0x00, 0x41, 0x7F, 0x41, 0x00, // 0x49 I
0x20, 0x40, 0x41, 0x3F, 0x01, // 0x4A J
0x7F, 0x08, 0x14, 0x22, 0x41, // 0x4B K
0x7F, 0x40, 0x40, 0x40, 0x40, // 0x4C L
0x7F, 0x02, 0x1C, 0x02, 0x7F, // 0x4D M
0x7F, 0x04, 0x08, 0x10, 0x7F, // 0x4E N
0x3E, 0x41, 0x41, 0x41, 0x3E, // 0x4F O
0x7F, 0x09, 0x09, 0x09, 0x06, // 0x50 P
0x3E, 0x41, 0x51, 0x21, 0x5E, // 0x51 Q
0x7F, 0x09, 0x19, 0x29, 0x46, // 0x52 R
0x26, 0x49, 0x49, 0x49, 0x32, // 0x53 S
0x03, 0x01, 0x7F, 0x01, 0x03, // 0x54 T
0x3F, 0x40, 0x40, 0x40, 0x3F, // 0x55 U
0x1F, 0x20, 0x40, 0x20, 0x1F, // 0x56 V
0x3F, 0x40, 0x38, 0x40, 0x3F, // 0x57 W
0x63, 0x14, 0x08, 0x14, 0x63, // 0x58 X
0x03, 0x04, 0x78, 0x04, 0x03, // 0x59 Y
0x61, 0x59, 0x49, 0x4D, 0x43, // 0x5A Z
0x00, 0x7F, 0x41, 0x41, 0x41, // 0x5B [
0x02, 0x04, 0x08, 0x10, 0x20, // 0x5C '\'
0x00, 0x41, 0x41, 0x41, 0x7F, // 0x5D ]
0x04, 0x02, 0x01, 0x02, 0x04, // 0x5E ^
0x40, 0x40, 0x40, 0x40, 0x40, // 0x5F _
0x00, 0x03, 0x07, 0x08, 0x00, // 0x60 `
0x20, 0x54, 0x54, 0x78, 0x40, // 0x61 a
0x7F, 0x28, 0x44, 0x44, 0x38, // 0x62 b
0x38, 0x44, 0x44, 0x44, 0x00, // 0x63 c
0x38, 0x44, 0x44, 0x28, 0x7F, // 0x64 d
0x38, 0x54, 0x54, 0x54, 0x18, // 0x65 e
0x00, 0x08, 0x7E, 0x09, 0x02, // 0x66 f
// 0x18, 0xA4, 0xA4, 0x9C, 0x78, // 0x67 g (old)
0x0C, 0x52, 0x52, 0x4E, 0x3C, // 0x67 g
0x7F, 0x08, 0x04, 0x04, 0x78, // 0x68 h
0x00, 0x44, 0x7D, 0x40, 0x00, // 0x69 i
0x20, 0x40, 0x40, 0x3D, 0x00, // 0x6A j
0x7F, 0x10, 0x28, 0x44, 0x00, // 0x6B k
0x00, 0x41, 0x7F, 0x40, 0x00, // 0x6C l
0x7C, 0x04, 0x78, 0x04, 0x78, // 0x6D m
0x7C, 0x08, 0x04, 0x04, 0x78, // 0x6E n
0x38, 0x44, 0x44, 0x44, 0x38, // 0x6F o
0xFC, 0x18, 0x24, 0x24, 0x18, // 0x70 p
// 0x18, 0x24, 0x24, 0x18, 0xFC, // 0x71 q (old)
0x0C, 0x12, 0x12, 0x0C, 0x7E, // 0x71 q
0x7C, 0x08, 0x04, 0x04, 0x08, // 0x72 r
0x48, 0x54, 0x54, 0x54, 0x24, // 0x73 s
0x04, 0x04, 0x3F, 0x44, 0x24, // 0x74 t
0x3C, 0x40, 0x40, 0x20, 0x7C, // 0x75 u
0x1C, 0x20, 0x40, 0x20, 0x1C, // 0x76 v
0x3C, 0x40, 0x30, 0x40, 0x3C, // 0x77 w
0x44, 0x28, 0x10, 0x28, 0x44, // 0x78 x
0x0C, 0x10, 0x10, 0x50, 0x3C, // 0x79 y
0x44, 0x64, 0x54, 0x4C, 0x44, // 0x7A z
0x00, 0x08, 0x36, 0x41, 0x00, // 0x7B {
0x00, 0x00, 0x7F, 0x00, 0x00, // 0x7C |
0x00, 0x41, 0x36, 0x08, 0x00, // 0x7D }
0x02, 0x01, 0x02, 0x04, 0x02, // 0x7E ~
0x3C, 0x26, 0x23, 0x26, 0x3C, // 0x7F house
0x1E, 0xA1, 0xA1, 0x61, 0x12, // 0x81
0x3A, 0x40, 0x40, 0x20, 0x7A, // 0x81 ü
0x38, 0x54, 0x54, 0x55, 0x59, // 0x82
0x21, 0x55, 0x55, 0x79, 0x41, // 0x83
0x21, 0x54, 0x54, 0x78, 0x41, // 0x84 ä
0x21, 0x55, 0x54, 0x78, 0x40, // 0x85
0x20, 0x54, 0x55, 0x79, 0x40, // 0x86
0x0C, 0x1E, 0x52, 0x72, 0x12, // 0x87
0x39, 0x55, 0x55, 0x55, 0x59, // 0x88
0x39, 0x54, 0x54, 0x54, 0x59, // 0x89
0x39, 0x55, 0x54, 0x54, 0x58, // 0x8A
0x00, 0x00, 0x45, 0x7C, 0x41, // 0x8B
0x00, 0x02, 0x45, 0x7D, 0x42, // 0x8C
0x00, 0x01, 0x45, 0x7C, 0x40, // 0x8D
0xF0, 0x29, 0x24, 0x29, 0xF0, // 0x8E Ä
0xF0, 0x28, 0x25, 0x28, 0xF0, // 0x8F
0x7C, 0x54, 0x55, 0x45, 0x00, // 0x90
0x20, 0x54, 0x54, 0x7C, 0x54, // 0x91
0x7C, 0x0A, 0x09, 0x7F, 0x49, // 0x92
0x32, 0x49, 0x49, 0x49, 0x32, // 0x93
0x32, 0x48, 0x48, 0x48, 0x32, // 0x94 ö
0x32, 0x4A, 0x48, 0x48, 0x30, // 0x95
0x3A, 0x41, 0x41, 0x21, 0x7A, // 0x96
0x3A, 0x42, 0x40, 0x20, 0x78, // 0x97
0x00, 0x9D, 0xA0, 0xA0, 0x7D, // 0x98
//0x39, 0x44, 0x44, 0x44, 0x39, // 0x99 Ö (old)
0x3D, 0x42, 0x42, 0x42, 0x3D, // 0x99 Ö
0x3D, 0x40, 0x40, 0x40, 0x3D, // 0x9A Ü
0x3C, 0x24, 0xFF, 0x24, 0x24, // 0x9B cent
0x48, 0x7E, 0x49, 0x43, 0x66, // 0x9C pound
0x2B, 0x2F, 0xFC, 0x2F, 0x2B, // 0x9D
0xFF, 0x09, 0x29, 0xF6, 0x20, // 0x9E
0xC0, 0x88, 0x7E, 0x09, 0x03, // 0x9F
0x20, 0x54, 0x54, 0x79, 0x41, // 0xA0
0x00, 0x00, 0x44, 0x7D, 0x41, // 0xA1
0x30, 0x48, 0x48, 0x4A, 0x32, // 0xA2
0x38, 0x40, 0x40, 0x22, 0x7A, // 0xA3
0x00, 0x7A, 0x0A, 0x0A, 0x72, // 0xA4
0x7D, 0x0D, 0x19, 0x31, 0x7D, // 0xA5
0x26, 0x29, 0x29, 0x2F, 0x28, // 0xA6
0x26, 0x29, 0x29, 0x29, 0x26, // 0xA7
0x30, 0x48, 0x4D, 0x40, 0x20, // 0xA8
0x38, 0x08, 0x08, 0x08, 0x08, // 0xA9
0x08, 0x08, 0x08, 0x08, 0x38, // 0xAA
0x2F, 0x10, 0xC8, 0xAC, 0xBA, // 0xAB
0x2F, 0x10, 0x28, 0x34, 0xFA, // 0xAC
0x00, 0x00, 0x7B, 0x00, 0x00, // 0xAD exclamation mark upside down
0x08, 0x14, 0x2A, 0x14, 0x22, // 0xAE less less
0x22, 0x14, 0x2A, 0x14, 0x08, // 0xAF greater greater
0xAA, 0x00, 0x55, 0x00, 0xAA, // 0xB0
0xAA, 0x55, 0xAA, 0x55, 0xAA, // 0xB1
0xAA, 0x55, 0xAA, 0x55, 0xAA, // 0xB2
0x00, 0x00, 0x00, 0xFF, 0x00, // 0xB3
0x10, 0x10, 0x10, 0xFF, 0x00, // 0xB4
0x14, 0x14, 0x14, 0xFF, 0x00, // 0xB5
0x10, 0x10, 0xFF, 0x00, 0xFF, // 0xB6
0x10, 0x10, 0xF0, 0x10, 0xF0, // 0xB7
0x14, 0x14, 0x14, 0xFC, 0x00, // 0xB8
0x14, 0x14, 0xF7, 0x00, 0xFF, // 0xB9
0x00, 0x00, 0xFF, 0x00, 0xFF, // 0xBA
0x14, 0x14, 0xF4, 0x04, 0xFC, // 0xBB
0x14, 0x14, 0x17, 0x10, 0x1F, // 0xBC
0x10, 0x10, 0x1F, 0x10, 0x1F, // 0xBD
0x14, 0x14, 0x14, 0x1F, 0x00, // 0xBE
0x10, 0x10, 0x10, 0xF0, 0x00, // 0xBF
0x00, 0x00, 0x00, 0x1F, 0x10, // 0xC0
0x10, 0x10, 0x10, 0x1F, 0x10, // 0xC1
0x10, 0x10, 0x10, 0xF0, 0x10, // 0xC2
0x00, 0x00, 0x00, 0xFF, 0x10, // 0xC3
0x10, 0x10, 0x10, 0x10, 0x10, // 0xC4
0x10, 0x10, 0x10, 0xFF, 0x10, // 0xC5
0x00, 0x00, 0x00, 0xFF, 0x14, // 0xC6
0x00, 0x00, 0xFF, 0x00, 0xFF, // 0xC7
0x00, 0x00, 0x1F, 0x10, 0x17, // 0xC8
0x00, 0x00, 0xFC, 0x04, 0xF4, // 0xC9
0x14, 0x14, 0x17, 0x10, 0x17, // 0xCA
0x14, 0x14, 0xF4, 0x04, 0xF4, // 0xCB
0x00, 0x00, 0xFF, 0x00, 0xF7, // 0xCC
0x14, 0x14, 0x14, 0x14, 0x14, // 0xCD
0x14, 0x14, 0xF7, 0x00, 0xF7, // 0xCE
0x14, 0x14, 0x14, 0x17, 0x14, // 0xCF
0x10, 0x10, 0x1F, 0x10, 0x1F, // 0xD0
0x14, 0x14, 0x14, 0xF4, 0x14, // 0xD1
0x10, 0x10, 0xF0, 0x10, 0xF0, // 0xD2
0x00, 0x00, 0x1F, 0x10, 0x1F, // 0xD3
0x00, 0x00, 0x00, 0x1F, 0x14, // 0xD4
0x00, 0x00, 0x00, 0xFC, 0x14, // 0xD5
0x00, 0x00, 0xF0, 0x10, 0xF0, // 0xD6
0x10, 0x10, 0xFF, 0x10, 0xFF, // 0xD7
0x14, 0x14, 0x14, 0xFF, 0x14, // 0xD8
0x10, 0x10, 0x10, 0x1F, 0x00, // 0xD9
0x00, 0x00, 0x00, 0xF0, 0x10, // 0xDA
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xDB
0xF0, 0xF0, 0xF0, 0xF0, 0xF0, // 0xDC
0xFF, 0xFF, 0xFF, 0x00, 0x00, // 0xDD
0x00, 0x00, 0x00, 0xFF, 0xFF, // 0xDE
0x0F, 0x0F, 0x0F, 0x0F, 0x0F, // 0xDF
0x38, 0x44, 0x44, 0x38, 0x44, // 0xE0 alpha
0x7C, 0x2A, 0x2A, 0x3E, 0x14, // 0xE1 beta / ß
0x7E, 0x02, 0x02, 0x06, 0x06, // 0xE2 Gamma
0x02, 0x7E, 0x02, 0x7E, 0x02, // 0xE3 Pi
0x63, 0x55, 0x49, 0x41, 0x63, // 0xE4 Sigma
0x38, 0x44, 0x44, 0x3C, 0x04, // 0xE5 sigma
0x40, 0x7E, 0x20, 0x1E, 0x20, // 0xE6 mu
0x06, 0x02, 0x7E, 0x02, 0x02, // 0xE7 tau
0x99, 0xA5, 0xE7, 0xA5, 0x99, // 0xE8 Phi
0x1C, 0x2A, 0x49, 0x2A, 0x1C, // 0xE9 theta
0x4C, 0x72, 0x01, 0x72, 0x4C, // 0xEA Omega
0x30, 0x4A, 0x4D, 0x4D, 0x30, // 0xEB delta
0x30, 0x48, 0x78, 0x48, 0x30, // 0xEC phi?
0xBC, 0x62, 0x5A, 0x46, 0x3D, // 0xED empty set
0x3E, 0x49, 0x49, 0x49, 0x00, // 0xEE epsilon
0x7E, 0x01, 0x01, 0x01, 0x7E, // 0xEF
0x2A, 0x2A, 0x2A, 0x2A, 0x2A, // 0xF0 equivalence
0x44, 0x44, 0x5F, 0x44, 0x44, // 0xF1 plus minus
0x40, 0x51, 0x4A, 0x44, 0x40, // 0xF2 greater or equal
0x40, 0x44, 0x4A, 0x51, 0x40, // 0xF3 less or equal
0x00, 0x00, 0xFF, 0x01, 0x03, // 0xF4
0xE0, 0x80, 0xFF, 0x00, 0x00, // 0xF5
0x08, 0x08, 0x6B, 0x6B, 0x08, // 0xF6 divided by
0x36, 0x12, 0x36, 0x24, 0x36, // 0xF7 approx
0x06, 0x0F, 0x09, 0x0F, 0x06, // 0xF8 degree
0x00, 0x00, 0x18, 0x18, 0x00, // 0xF9 big dot
0x00, 0x00, 0x10, 0x10, 0x00, // 0xFA small dot
0x30, 0x40, 0xFF, 0x01, 0x01, // 0xFB square root
0x00, 0x1F, 0x01, 0x01, 0x1E, // 0xFC
0x00, 0x19, 0x1D, 0x17, 0x12, // 0xFD power of two
0x00, 0x3C, 0x3C, 0x3C, 0x3C, // 0xFE blob
//0x00, 0x00, 0x00, 0x60, 0x60, // 0xFF: Decimal point: 4 points
0x00, 0x00, 0x00, 0x00, 0x40, // 0xFF: Decimal point, only 1 point
};
void setup_pins() {
DPRINTLN("VQC10 Clock - setup 74HC595 pins");
pinMode(latchPin, OUTPUT); regPin(latchPin,"latchPin");
pinMode(dataPin,OUTPUT); regPin(dataPin,"dataPin");
pinMode(clockPin,OUTPUT); regPin(clockPin,"clockPin");
pinMode(D5pin,INPUT); regPin(D5pin,"D5pin"); // NOTE: configured and registered but never read anywhere in this driver or the core file - purpose (spare sensor/button input?) is undocumented, see open questions
digitsOnly = false; // this driver can render full ASCII text (dot-matrix font), not just 0-9, so let the core firmware know
asciiConvert[16] = 9; // runtime override: digit-buffer code 16 (originally mapped to '~' above) is remapped to raw font index 9 ("small empty circle" glyph, see _font table) for a driver-specific extra symbol
// Deliberately does NOT call startTimer() here (unlike most other drivers): the hardware
// timer ISR (writeDisplay(), see below) is intentionally a no-op for VQC10, and indeed
// startTimer() is never invoked anywhere for this driver in the whole project. All actual
// refresh work happens cooperatively via writeDisplay2(), called repeatedly from loop()/
// Fdelay() in the core file - see the comment above writeDisplay() for why.
}
void inline shiftOutForward(byte myDataOut) {
// This shifts 8 bits out MSB first, on the rising edge of the clock, clock idles low
// Used for the "digit select" (Z) and "row select" shift-register bytes, whose bit
// order must match how those select lines are physically wired to the 74HC595 outputs.
for (int i=7; i>=0; i--) {
digitalWrite(clockPin, 0);
digitalWrite(dataPin, myDataOut & (1<<i)); //register shifts bits on upstroke of clock pin
digitalWrite(clockPin, 1);
digitalWrite(dataPin, 0);
}
}
void inline shiftOutReverse(byte myDataOut) {
// This shifts 8 bits out LSB first, on the rising edge of the clock, clock idles low
// Used for the pixel/column DATA byte only - the opposite bit order from
// shiftOutForward() is required here because the data byte travels through the same
// shift-register chain but ends up latched onto a different (mirrored) set of output
// pins than the select bytes; using the wrong helper here would mirror the glyph.
for (int i=0; i<8; i++) {
digitalWrite(clockPin, 0);
digitalWrite(dataPin, myDataOut & (1<<i)); //register shifts bits on upstroke of clock pin
digitalWrite(clockPin, 1);
digitalWrite(dataPin, 0);
}
}
#define PWMrefresh 8000l // NOTE: defined but not referenced anywhere in this file - dead macro (leftover from an earlier PWM implementation?)
// Required interface stub only: the hardware timer ISR that drives every other driver's
// multiplex refresh is intentionally left EMPTY for VQC10. The real refresh (writeDisplay2()
// below) needs delayMicroseconds() calls per row for PWM brightness dimming, which is far too
// slow/blocking to run safely inside a timer ISR context. Instead, writeDisplay2() is called
// cooperatively from the main loop() and from Fdelay() in the core file. Consistent with this,
// setup_pins() above never calls startTimer(), so this ISR is in fact never even attached for
// this driver - it exists purely to satisfy the common driver interface (extern declaration in
// the core file, and the VQC10 special-case in changeDigit()'s animation code).
void ICACHE_RAM_ATTR writeDisplay() {}
int PWM_min = 0;
int PWM_max = 800; // row on-time ceiling in microseconds, at full brightness (see PWMtimeBrightness below)
// Blocking, cooperative multiplex refresh for the whole 8x5x7 display - one full call
// scans every row once. Called repeatedly from loop()/Fdelay() in the core file (NOT from
// an ISR - see the note above writeDisplay()). Brightness is implemented as simple PWM: each
// row is held "on" for PWMtimeBrightness microseconds out of a PWM_max budget, then forced
// off for the remainder, so total per-row time is constant regardless of brightness (keeps
// the row scan rate/refresh flicker frequency stable while only the mark/space ratio changes).
void writeDisplay2() {
static int PWMtimeBrightness;
//static uint32_t refresh = 0;
//static uint32_t runtim = 0;
//static uint32_t laststart = 0;
boolean offState;
int brightness;
int iRow,loopMax;
//refresh = millis()-laststart;
//laststart = millis();
//runtim = laststart;
brightness = displayON ? prm.dayBright : prm.nightBright;
if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
if (autoBrightness && displayON) {
PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX); // ambient-light-driven dimming: scale row on-time by measured lux vs. MAXIMUM_LUX
}
else
PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS); // manual brightness: scale row on-time by configured day/night brightness level
iRow = 0;
// FIXED (was "loopMax = maxDigits;", i.e. NUMDIGITS=8): this loop indexes dot-matrix ROWS, which
// only range 0..NUMROWS-1 (NUMROWS=7, i.e. dat[i] is sized [NUMROWS]=[7]), not digit columns. The
// old bound made the loop run iRow = 0..7 (8 iterations) instead of 0..6 (7 iterations), so on the
// last iteration (iRow==7) "dat[i][iRow]" below read one element past the end of each digit's
// 7-byte row array - for i<NUMDIGITS-1 this silently read dat[i+1][0] (the next digit's row-0
// data, due to row-major layout) instead of a real row-7 pattern, and for the last digit
// (i==NUMDIGITS-1) it read out of the whole `dat` array into whatever memory followed it
// (oldDat[0][0]). It also drove a nonexistent 8th row-select line via shiftOutForward(~(1<<iRow))
// below. Net effect was an extra, garbage 8th "row" scanned on every refresh - flicker/ghosting
// risk and a stolen slice of the row-time PWM budget.
loopMax = NUMROWS;
do {
// NOTE: the "iRow == maxDigits" term here can never be true (maxDigits==NUMDIGITS==8, but iRow now
// only ever reaches up to NUMROWS-1==6 before the loop exits via `while(iRow<loopMax)`) - it looks
// like it was meant to add an extra "sync/blank" row at the end of each sweep, but as written it's
// dead weight in the OR. Left as-is (not an out-of-bounds/indexing issue, just an inert condition).
offState = ( (brightness == 0) || (!radarON) || (iRow == maxDigits));
for (int i=0;i<NUMDIGITS;i++) { //fill one line with dots
// Two-phase shift+latch per digit: first phase asserts this digit's select line
// together with its row data; the second phase re-latches the SAME data but with
// ALL digit-select lines forced back off (0xFF = all deselected, active-low select).
// This immediately blanks the just-lit digit again before the loop moves on to the
// next one, which prevents the still-settling row data from bleeding/ghosting onto
// the previously selected digit while the next digit's select+data is being shifted
// in (VFD/tube grids can retain enough charge to glow briefly if left selected while
// data changes underneath them).
shiftOutForward(0xFF); //deselect Z
shiftOutForward(~(1<<i)); //select digit
shiftOutReverse(offState ? 0 : dat[i][iRow]); // set data line
digitalWrite(latchPin, HIGH); //Write shift register to latch
digitalWrite(latchPin, LOW);
asm volatile ("nop");
//Finish latching
shiftOutForward(0xFF); //deselect Z
shiftOutForward(0xFF); //All digit select lines are HIGH
shiftOutReverse(offState ? 0 : dat[i][iRow]); //keep data line
digitalWrite(latchPin, HIGH); //Write shift register to latch
digitalWrite(latchPin, LOW);
asm volatile ("nop");
}
// All 8 digits now hold row iRow's column data latched into their driver ICs (but with
// digit-select left off from the blanking step above). Now pulse on the single row-select
// line for this row - this is what actually makes the row iRow of every digit light up
// simultaneously - for a PWM-timed on-period, implementing brightness control.
shiftOutForward(~(1<<iRow)); //enable row
shiftOutForward(0xFF); //All digit select lines are HIGH
shiftOutForward(0); //anything
digitalWrite(latchPin, HIGH); //Write shift register to latch
digitalWrite(latchPin, LOW);
if (!offState)
delayMicroseconds(PWMtimeBrightness);
else
delayMicroseconds(PWM_max-PWMtimeBrightness);
iRow++;
} while(iRow<loopMax);
//runtim = millis()-runtim;
//DPRINT(refresh); DPRINT("/"); DPRINTLN(runtim);
yield(); // hand control back to the RTOS/WiFi stack between refresh passes - important since this whole function runs blocking on the main loop, not in an ISR
}
// Looks up character c's glyph in the column-major _font[] table (5 bytes per glyph, one
// byte per column, each byte's low 7 bits = one dot per row) and converts it into the
// row-major representation (row[NUMROWS], one byte per row, one bit per column) that the
// row-scanned hardware needs. c is a plain ASCII code (0x00-0xFF), already translated from
// the core firmware's digit-buffer code via asciiConvert[] by the caller.
void show(byte c){
byte loccol[NUMCOLS];
for (byte i=0; i < NUMCOLS; i++)
loccol[i] = _font[c*NUMCOLS+i];
transposePattern(loccol,row);
}
// Transposes a column-major 5x7 glyph (cols[NUMCOLS], one bit per row within each column
// byte) into a row-major bitmap (rows[NUMROWS], one bit per column within each row byte).
// Each output row byte ends up with its NUMCOLS=5 column bits packed into bits [2..6]
// (via the final "<< (7-NUMCOLS)" shift), bit0/bit1/bit7 left as 0 - this alignment must
// match how those bits are wired through the 74HC595 chain to the physical column drivers.
// When _upsidedown is set, both the row scan direction (cmask shifted right instead of
// left, starting from bit6 instead of bit0) and the column read order are reversed, giving
// a full 180-degree flip for tubes mounted upside down (see _upsidedown declaration above -
// currently unused/dead in this project, always false).
void transposePattern(byte cols[], volatile byte rows[]){
byte cmask;
cmask = (_upsidedown ? 0x40 : 1);
for (byte r=0; r < NUMROWS; r++) {
rows[r] = 0;
for (byte c=0; c < NUMCOLS; c++) {
if (cmask&cols[(_upsidedown ? (NUMCOLS-c-1) : c)]) rows[r] |= 1;
rows[r] = rows[r] << 1;
}
if (_upsidedown) cmask = cmask >> 1;
else cmask = cmask << 1;
rows[r] = rows[r] << (7-NUMCOLS);
}
}
// Debug helper: prints one byte as 8 ASCII '0'/'1' characters (MSB first).
void bPrint(byte in) {
for (int i=7;i>=0;i--) {
boolean b = in & (1<<i);
DPRINT(b ? '1' : '0');
}
}
// Debug helper: dumps the current dat[][] buffer (the exact bitmap about to be scanned out
// by writeDisplay2()) to Serial as ASCII art, for bring-up/troubleshooting. Not called in
// normal operation (see the commented-out call at the end of writeDisplaySingle() below).
void printBitmap() { //print bitmap for testing
for (int j=0;j<NUMROWS;j++) {
for (int i=0;i<NUMDIGITS;i++) {
bPrint(dat[i][j]); Serial.print(" ");
}
DPRINTLN(" ");
}
DPRINTLN("__________________________________");
}
// No-op: unlike several other drivers, VQC10 has no dedicated "all outputs off" hardware
// shortcut here - blanking a digit position is instead achieved through the normal buffer
// path by writing digit-buffer code 10 (BLANK) into digit[]/newDigit[], which asciiConvert[]
// maps to a space character (empty glyph). The core .ino no longer calls clearTubes() from
// anywhere at all (its former caller, disableDisplay(), has been removed entirely as dead code),
// so this stub is purely for interface compliance at this point.
void clearTubes() {}
// Blocking, one-shot render: rebuilds oldDat[]/newDat[] (bitmaps for the outgoing and
// incoming character of every digit position) from the shared digit[]/newDigit[] buffers,
// then blends them into dat[] according to each position's animMask[] wipe-animation
// progress. dat[] is what writeDisplay2() actually scans out to the hardware - this
// function does not touch the hardware itself.
void writeDisplaySingle() {
char dispChar;
// Physical-to-logical digit position remapping: the 8 cells are wired/driven as two
// groups of 4 in reverse order (3,2,1,0 then 7,6,5,4), consistent with two physical
// VQC10 modules (see CLOCK_71 "2x VQC10 clock") each internally mirrored/chained back to
// front relative to the logical left-to-right digit[]/newDigit[] buffer order.
byte tubeShift[] = {3,2,1,0,7,6,5,4};
byte a;
for (int i=0;i<NUMDIGITS;i++) { //generate new line -- NOTE: comment says "new" but this loop actually builds oldDat[] (the OUTGOING/current character) from digit[]; see the next loop for newDat[]/newDigit[]. Comment/variable naming appears swapped from actual old/new roles, though the logic itself is correct (oldDat<-digit[], newDat<-newDigit[] matches the core file's old/new buffer convention).
dispChar = digit[tubeShift[i]];
if (dispChar<sizeof(asciiConvert)) dispChar = asciiConvert[(byte)dispChar]; // translate digit-buffer code to ASCII via asciiConvert[]; note dispChar is signed char compared against unsigned sizeof() - harmless here since digit-buffer codes are always small non-negative values, but a latent signed/unsigned mismatch
show(dispChar);
for (int k=0;k<NUMROWS;k++) {
oldDat[i][k] = row[k];
}
if (digitDP[tubeShift[i]]) {
show(DPchar); // overlay the decimal-point/separator dot glyph
for (int k=0;k<NUMROWS;k++) {
oldDat[i][k] |= row[k]; // OR the DP dot pattern onto the character pattern so both render together in the same cell
}
}
}
for (int i=0;i<NUMDIGITS;i++) { //generate old line -- NOTE: comment says "old" but this loop builds newDat[] (the INCOMING/target character) from newDigit[]; see note on the previous loop.
dispChar = newDigit[tubeShift[i]];
if (dispChar<sizeof(asciiConvert)) dispChar = asciiConvert[(byte)dispChar];
show(dispChar);
for (int k=0;k<NUMROWS;k++) {
newDat[i][k] = row[k];
}
if (digitDP[tubeShift[i]]) {
show(DPchar);
for (int k=0;k<NUMROWS;k++) {
newDat[i][k] |= row[k];
}
}
}
// Vertical wipe/scroll transition: blend oldDat[] and newDat[] into dat[] based on
// animMask[] (set by the core file's changeDigit() animation state machine, incrementing
// roughly 1..19 over the transition - see the VQC10 special-case in that function). a=0
// means no animation in progress yet (show old character unchanged); as a increases
// (halved and capped to NUMROWS-1=7 dot-rows) the display effectively scrolls the old
// character's rows upward out of view while the new character's rows scroll in from the
// bottom, row-by-row, producing a smooth vertical wipe rather than an instant character
// swap (this is the driver-specific animation timing referenced by the VQC10 special-case
// in the core file's changeDigit()).
for (int i=0;i<NUMDIGITS;i++) { //combine animation
a = animMask[tubeShift[i]]/2; if (a>7) a=7;
for (int k=0;k<NUMROWS;k++) {
if (a==0) dat[i][k] = oldDat[i][k];
else { //Animation!!!
if ((a+k)<NUMROWS) {
dat[i][k] = oldDat[i][k+a]; // still within old character's remaining (not yet scrolled out) rows
}
else {
dat[i][k] = newDat[i][(k+a)-NUMROWS]; // wrapped past the bottom: pull in rows of the new character instead
}
} //endif Animation!!
} //end for i
} //end for
//printBitmap();
}
#endif