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>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-25 20:48:04 +01:00
1 parent 8efa810e4e
commit b347f1c63f
843 files changed
+122061

No files matched your search

@@ -0,0 +1,361 @@
#ifdef PT6311
//===========================================================================================
// Driver for the Princeton Technology PT6311 VFD controller/driver IC.
//
// Hardware / protocol:
// The PT6311 is a "smart" VFD grid/segment controller: unlike a dumb shift-register driver
// (e.g. MAX6921) it contains its own display RAM and internal multiplex/scan generator, so
// the MCU only has to push a full frame into it after something changes -- the chip itself
// keeps refreshing the tubes autonomously in the background. That is why writeDisplay()
// (the timer-ISR hook used by "dumb" drivers) is an empty stub here and setup_pins() never
// calls startTimer(): there is no ISR-driven multiplex loop for this driver, all display
// updates go through the blocking writeDisplaySingle().
// 3-wire serial bus (similar to SPI, chip-specific framing):
// PIN_LOAD (STB/CS) - LOW while a command/data byte is being shifted in, HIGH between
// bytes to latch/strobe it. Pulled LOW again for every single byte,
// i.e. each byte shifted is individually framed by its own STB pulse
// (see shift()/sendCommand()/writeDisplaySingle()).
// PIN_CLK - one pulse per bit.
// PIN_DIN - serial data, LSB-first per byte (see shift(): loop starts at
// bit 0), which is the opposite bit order of the MAX6921 driver.
// Command bytes (C1..C4) select a mode: C1 sets 8-digit/20-segment display mode, C2 selects
// "write display data, auto-increment address", C3 sets the RAM write address, C4 sets
// brightness/on-off. Display RAM is addressed per digit in steps of 3 (see
// C3_ADDRESS_SETTING + i*3 in writeDisplaySingle()) because the chip reserves 3 bytes per
// digit position in its internal RAM regardless of how many of those bits are actually
// wired to segments on this PCB.
//
// Used by (see clocks.h, search for "#define PT6311"):
// CLOCK_17 - ESP8266 + PT6311, 4x IV-22 tubes, "russian clock"
//===========================================================================================
char tubeDriver[] = "PT6311";
int maxDigits = sizeof(digitEnablePins); //digitEnablePins is a byte[] (1 byte/element), so sizeof() == element count here
int wt = 1; //Serial timing
//#define PIN_CLK 14 // Clock
//#define PIN_DIN 13 // DataIN
//#define PIN_LOAD 15 // STB pin
#define SEGMENT8 //if not used, 17 segments or more
//#define TESTMODE //for testing the segments of a new type VFD module
//PT6311 Commands
// Display-control command per brightness step 0(OFF)..10. Bits 3-1 of the low nibble select
// one of the chip's 8 pulse-width/duty codes (000..111); with 10 non-zero UI brightness
// levels mapped onto only 8 hardware codes, two steps had to collapse onto the same code.
// POSSIBLE BUG: index 5 and 6 (below) both encode 0B10001100, and index 8 and 9 both encode
// 0B10001110 -- i.e. brightness levels 5/6 and 8/9 are indistinguishable on the hardware.
// This may be intentional (skipping a step to line the mapping up some other way) or a
// copy-paste slip when the table was typed out; worth confirming against the datasheet.
byte DISPLAY_MODE[11] = {
0B00000000, //OFF
0B10001000, //1
0B10001001, //2
0B10001010, //3
0B10001011, //4
0B10001100, //5
0B10001100, //6 // POSSIBLE BUG: identical to level 5 above -- see note above the array
0B10001101, //7
0B10001110, //8
0B10001110, //9 // POSSIBLE BUG: identical to level 8 above -- see note above the array
0B10001111, //10
};
#define C1_DISPLAY_MODE_SETTING B00000000 //8 digit, 20 segments
#define C2_DATA_SETTING B01000000 //write data to display, addr increment
#define C2_DATA_SETTING_TESTMODE B01001000 //write data to display, addr increment
#define C3_ADDRESS_SETTING B11000000 //Address setting to 0
#define C4_DISPLAY_CONTROL B10001111 //max brightness, ON (kept for reference; actual brightness command is looked up from DISPLAY_MODE[] instead)
#ifdef SEGMENT8
//------------------abcdefgDP---------------- definition of different characters
// 8-bit segment mask per glyph, MSB-first as a,b,c,d,e,f,g,DP (bit7=a .. bit0=DP).
// Indices 0-9 = digits, 10 = blank, 11 = minus, 12 = decimal point, 13+ = extra glyphs
// (A,P,C,deg,%%,lower-circle,I,F) -- this indexing matches the digit[]/newDigit[] encoding
// used by the core firmware (see ESP32_UniClock2.ino), so digit[] values map 1:1 into this
// table without any translation layer (unlike the ASCII/16-segment branch below).
#define MAXSEGMENTS 8
boolean ASCIImode = false;
byte charDefinition[] = {
B11111100, //0: abcdef
B01100000, //1: bc
B11011010, //2: abdeg
B11110010, //3: abcdg
B01100110, //4: bcfg
B10110110, //5: acdfg
B10111110, //6: acdefg
B11100000, //7: abc
B11111110, //8: abcdefg
B11110110, //9: abcdfg
B00000000, // : BLANK (10)
B00000010, //-: minus (11)
B00000001, // decimal point (12)
B11101110, // A abcefg (13)
B11001110, // P abefg (14)
B10011100, // C adef (15)
B11000110, //grad (upper circle) abfg (16)
B10110100, //% acdf (17)
B00111010, //lower circle cdeg (18)
B01100000, //I bc (19)
B10001110 //F aefg (20)
};
//byte segmentEnablePins[] = {9,8,5,3,4,7,6,2}; //segment enable bits (a,b,c,d,e,f,g,DP) (You MUST define always 8 bits!!!)
#else //16 segment + DP ---------------------------------------------------------------------------------------------
#define C1_DISPLAY_MODE_SETTING B00000000 //8 digit, 20 segments
#define MAXSEGMENTS 17
boolean ASCIImode = true;
// Full 16-segment-star + DP font, indexed by ASCII code - 32 (i.e. index 0 = space/0x20).
// Since digit[]/newDigit[] hold the compact 0-9/BLANK/minus/DP/glyph codes (not ASCII), the
// ASCIImode path in writeDisplaySingle() first maps digit[i] through asciiConvert[] to get an
// actual ASCII character, then subtracts 32 to index into this table.
uint32_t charDefinition[96] = {
0b00000000000000000, /* (space) */
0b10000000000001100, /* ! */
0b00000001000000100, /* " */ // 100000001
0b01010101000111100, /* # */
0b01010101010111011, /* $ */
0b01110111010011001, /* % */
0b01001001101110001, /* & */
0b00000001000000000, /* ' */
0b00001010000000000, /* ( */
0b00100000100000000, /* ) */
0b01111111100000000, /* * */
0b01010101000000000, /* + */
0b00100000000000000, /* , */
0b01000100000000000, /* - */
0b10000000000000000, /* . */
0b00100010000000000, /* / */
0b00100010011111111, /* 0 */
0b00000010000001100, /* 1 */
0b01000100001110111, /* 2 */
0b00000100000111111, /* 3 */
0b01000100010001100, /* 4 */
0b01001000010110011, /* 5 */
0b01000100011111011, /* 6 */
0b00000000000001111, /* 7 */
0b01000100011111111, /* 8 */
0b01000100010111111, /* 9 */
0b00010001000000000, /* : */
0b00100001000000000, /* ; */
0b01001010000000000, /* < */
0b01000100000110000, /* = */
0b00100100100000000, /* > */
0b10010100000000111, /* ? */
0b00000101011110111, /* @ */
0b01000100011001111, /* A */
0b00010101000111111, /* B */
0b00000000011110011, /* C */
0b00010001000111111, /* D */
0b01000000011110011, /* E */
0b01000000011000011, /* F */
0b00000100011111011, /* G */
0b01000100011001100, /* H */
0b00010001000110011, /* I */
0b00000000001111100, /* J */
0b01001010011000000, /* K */
0b00000000011110000, /* L */
0b00000010111001100, /* M */
0b00001000111001100, /* N */
0b00000000011111111, /* O */
0b01000100011000111, /* P */
0b00001000011111111, /* Q */
0b01001100011000111, /* R */
0b01000100010111011, /* S */
0b00010001000000011, /* T */
0b00000000011111100, /* U */
0b00100010011000000, /* V */
0b00101000011001100, /* W */
0b00101010100000000, /* X */
0b01000100010111100, /* Y */
0b00100010000110011, /* Z */
0b00010001000010010, /* [ */
0b00001000100000000, /* \ */
0b00010001000100001, /* ] */
0b00101000000000000, /* ^ */
0b00000000000110000, /* _ */
0b00000000100000000, /* ` */
0b01010000001110000, /* a */
0b01010000011100000, /* b */
0b01000000001100000, /* c */
0b00010100000011100, /* d */
0b01100000001100000, /* e */
0b01010101000000010, /* f */
0b01010001010100001, /* g */
0b01010000011000000, /* h */
0b00010000000000000, /* i */
0b00010001001100000, /* j */
0b00011011000000000, /* k */
0b00000000011000000, /* l */
0b01010100001001000, /* m */
0b01010000001000000, /* n */
0b01010000001100000, /* o */
0b01000001011000001, /* p */
0b01010001010000001, /* q */
0b01000000001000000, /* r */
0b01010000010100001, /* s */
0b01000000011100000, /* t */
0b00010000001100000, /* u */
0b00100000001000000, /* v */
0b00101000001001000, /* w */
0b00101010100000000, /* x */
0b00000101000011100, /* y */
0b01100000000100000, /* z */
0b01010001000010010, /* { */
0b00010001000000000, /* | */
0b00010101000100001, /* } */
0b01100110000000000, /* ~ */
0b00000000000000000, /* (del) */
};
//byte segmentEnablePins[] = {16,7,0,1,2,3,4,5,6,8,9,11,10,12,13,14,15}; //segment enable bits
#endif
// Maps the special glyph codes (BLANK, minus, DP, A,P,C,deg,%,circle,I,F -- digit[] values
// 10..20) onto printable ASCII characters, used only when ASCIImode is true (16-segment
// branch) so the digit[i] index can be turned into an ASCII char before it is used to look up
// charDefinition[]. Note this string is 1:1 positional with the 10..20 glyph codes, i.e.
// asciiConvert[0] corresponds to digit value 0 ('0'), asciiConvert[10] (' ') to BLANK, etc.
char asciiConvert[] = "0123456789 -.APC~%oIF";
#define MAXCHARS sizeof(charDefinition)/sizeof(charDefinition[0])
// Bit-bangs one byte out on PIN_DIN/PIN_CLK. NOTE: LSB-first (i starts at 0, tests bit 1<<i
// from the low end) -- the PT6311 expects the least-significant bit of each command/data byte
// first, which is the reverse of many other shift-register based drivers in this project.
void inline shift(byte Data) {
for (int i=0;i<8;i++) {
digitalWrite(PIN_CLK,LOW);
delayMicroseconds(2);
digitalWrite(PIN_DIN, Data & (1<<i));
delayMicroseconds(5);
digitalWrite(PIN_CLK,HIGH);
delayMicroseconds(5);
digitalWrite(PIN_CLK,LOW);
delayMicroseconds(2);
}
}
// Sends a single command/address byte framed by its own STB (PIN_LOAD) pulse: LOW while
// shifting, HIGH afterwards to latch it into the chip. Used for the C1/C3 setup commands;
// writeDisplaySingle() below repeats this same LOW/shift/HIGH framing manually per-byte
// instead of reusing this helper, since it needs to keep PIN_LOAD low across the multi-byte
// per-digit data burst.
void ICACHE_RAM_ATTR sendCommand(byte val){ //for commands C1..C3
digitalWrite(PIN_LOAD,LOW);
delayMicroseconds(2);
shift(val);
digitalWrite(PIN_LOAD,HIGH);
delayMicroseconds(2);
//DPRINT(address,HEX); DPRINT(","); DPRINTLN(val,HEX);
}
void setup_pins() {
DPRINTLN("PT6311 Clock - Setup pins...");
pinMode(PIN_LOAD,OUTPUT); regPin(PIN_LOAD,"PIN_LOAD");
pinMode(PIN_DIN, OUTPUT); regPin(PIN_DIN,"PIN_DIN");
pinMode(PIN_CLK, OUTPUT); regPin(PIN_CLK,"PIN_CLK");
digitalWrite(PIN_CLK, HIGH);
digitalWrite(PIN_LOAD, HIGH);
// Hardcoded to match the SEGMENT8 charDefinition table currently active above (only digits
// + '%' are used by the core when digitsOnly==true, see ESP32_UniClock2.ino). If SEGMENT8
// is ever disabled to switch this driver to the 16-segment/ASCIImode table, this should
// probably become "digitsOnly = !ASCIImode;" instead of staying hardcoded true.
digitsOnly = true;
delay(10);
sendCommand(C1_DISPLAY_MODE_SETTING);
}
#if defined(ESP32)
void IRAM_ATTR writeDisplaySingle(){
#else
void ICACHE_RAM_ATTR writeDisplaySingle(){
#endif
byte newBright = 0;
uint32_t bitBuffer;
int dispChar;
static int count = 15;
newBright = displayON ? prm.dayBright : prm.nightBright;
if (newBright>10) newBright = 10; //clamp: DISPLAY_MODE[] only has 11 entries (indices 0..10)
digitalWrite(PIN_LOAD, LOW); //safety only
delayMicroseconds(2);
// C2_DATA_SETTING switches the chip into "write display data / auto-increment address"
// mode; DISPLAY_MODE[newBright] is then a second command byte (not display data) setting
// brightness/on-off -- both are sent back-to-back inside the same PIN_LOAD-low window
// opened just above.
sendCommand(C2_DATA_SETTING);
sendCommand(DISPLAY_MODE[newBright]);
for (int i=0;i<maxDigits;i++) {
dispChar = digit[i];
if (ASCIImode) {
// FIXED: previously "dispChar -= 32" ran unconditionally, even when the bounds check just
// below skipped the asciiConvert[] lookup (dispChar >= sizeof(asciiConvert), or dispChar==21
// landing on the string's NUL terminator) - that produced a negative index into
// charDefinition[] a few lines down (out-of-bounds read). Now the subtraction only happens
// together with a successful lookup; anything unmapped falls back to charDefinition[0], the
// blank/space glyph, instead of indexing out of bounds.
if (dispChar<sizeof(asciiConvert)-1) { //-1 excludes the string's NUL terminator (see asciiConvert[] above)
dispChar = asciiConvert[dispChar] - 32;
} else {
dispChar = 0; //unmapped/out-of-range code -> blank glyph (charDefinition[0])
}
}
bitBuffer = 0;
digitalWrite(PIN_LOAD,LOW);
delayMicroseconds(2);
// Address increments by 3 per digit (not 1) -- the PT6311 reserves 3 RAM bytes per digit
// position internally (enough for up to 24 segment bits), even though this build only
// uses 1-2 of them (MAXSEGMENTS is 8 or 17, see charDefinition table above).
shift(C3_ADDRESS_SETTING + i*3); //set display address
for (int j=0;j<MAXSEGMENTS;j++)
if ((charDefinition[dispChar] & (uint32_t)1<<(MAXSEGMENTS-1-j)) != 0) {
bitBuffer |= (uint32_t)1<<segmentEnablePins[j];
}
if (digitDP[i]) bitBuffer |= (uint32_t)1<<segmentEnablePins[7]; //Decimal Point
//DPRINT(charDefinition[dispChar],BIN); DPRINT(" / "); DPRINTLN(bitBuffer,BIN);
if (newBright==0) bitBuffer = 0; //switch OFF display
// Always shifts 2 (or 3) bytes per digit regardless of MAXSEGMENTS, even in 8-segment
// mode where only the low byte carries real segment bits -- the chip expects a fixed
// per-digit byte count matching the RAM stride set above (i*3), so the extra byte(s) of
// padding are required protocol filler, not wasted work.
shift(bitBuffer & 0xFF);
shift(bitBuffer >>8);
if (MAXSEGMENTS>16) shift(bitBuffer >>16);
delayMicroseconds(2);
digitalWrite(PIN_LOAD,HIGH);
delayMicroseconds(2);
} //end for i
/* //TEST mode
digitalWrite(PIN_LOAD, LOW); //upload data
delayMicroseconds(2);
shift(C3_ADDRESS_SETTING);
for (int i=0;i<48;i++) {
shift(0xff); // Data to fill table 5*16 = 80 bits
//shift(0x00); // Data to fill table 5*16 = 80 bits
//shift(0xff); // Data to fill table 5*16 = 80 bits
}
digitalWrite(PIN_LOAD, HIGH);
delay(2);
*/
}
/*********************** Send command without strobe/chip select******************************************/
// No-op: this driver has no timer-ISR multiplex loop (see file header) -- the PT6311 refreshes
// its own tubes internally, so there is nothing for the periodic writeDisplay() ISR hook to do.
void writeDisplay() {}
// clearTubes() is a required part of the common driver interface, but nothing in the core .ino
// calls it any more (its only former caller, disableDisplay(), has been removed entirely as dead
// code) - kept as a no-op stub purely for interface compliance. If a synchronous immediate-blank is
// ever needed again, this would have to actively send an all-blank frame instead.
void clearTubes() {}
#endif