Files
nixie-clock/firmware/uniclock/ESP32_UniClock2/VQC10.ino
T

625 lines
31 KiB
Arduino
Raw Normal View History

2026-10-25 20:48:04 +01:00
// ============================================================================
// 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