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>
279 lines
14 KiB
Arduino
279 lines
14 KiB
Arduino
/*==============================================================================
|
|
MM5450.ino - driver for the MM5450 (National Semi / clone) LED segment driver IC.
|
|
|
|
Hardware: MM5450 is a 34-output constant-current LED sink driver with a
|
|
simple synchronous serial input (start bit + 34 data bits, latched with a
|
|
LE/latch-enable pulse). It has NO on-chip digit multiplexing and NO
|
|
brightness/intensity register - unlike MAX7219. In this board design a
|
|
single MM5450 is time-shared between TWO groups of 4x 7-segment+DP digits
|
|
(32 segment lines + 2 "side enable" lines = 34 outputs), and brightness is
|
|
produced entirely in software via PWM (see writeDisplay()/PWMrefresh below),
|
|
driven from the periodic hardware-timer ISR started by startTimer().
|
|
|
|
Wiring/protocol: DATA/CLK/LE are bit-banged (not hardware SPI) directly via
|
|
ESP8266 GPIO-set/clear peripheral registers for speed, because writeBits()
|
|
runs inside the timer ISR. Bits are shifted out in ascending bitBuffer[]
|
|
index order (index 0 first), each element corresponding 1:1 to one MM5450
|
|
output (index = output pin number as defined by the MM5450 datasheet,
|
|
1..34), with bitBuffer[0] used as the fixed "start bit" required by the
|
|
chip's serial protocol to frame the following 34 data bits.
|
|
|
|
Used by (see clocks.h): CLOCK_11 - "8266 LED Clock with MM5450 chip"
|
|
(clocks.h defines MM5450 only inside the #ifdef CLOCK_11 block).
|
|
==============================================================================*/
|
|
#ifdef MM5450
|
|
char tubeDriver[] = "MM5450";
|
|
int maxDigits = 6;
|
|
|
|
//Fill this table with the pin numbers of the MM5450 chip!
|
|
// Maps [digit-group 0..3][segment a..g,DP] -> MM5450 output pin number (1..34).
|
|
// Digit groups 0,1 belong to display "side" 0, groups 2,3 belong to "side" 1
|
|
// (see sideEnablePins[] and the side-switching logic in writeDisplay()).
|
|
byte segmentEnablePins[4][8] = {
|
|
{16, 15, 14, 13, 12, 11, 10, 9}, //segment enable pins of MM5450 (a,b,c,d,e,f,g,DP)
|
|
{8, 7, 6, 5, 4, 3, 2, 1},
|
|
{17, 18, 19, 20, 21, 22, 23, 24},
|
|
{25, 26, 27, 28, 29, 30, 31, 32}
|
|
};
|
|
|
|
|
|
/* Old version - kept for reference only, NOT compiled (different pin-to-output mapping,
|
|
presumably from an earlier PCB revision with different MM5450 output wiring).
|
|
byte segmentEnablePins[4][8] = {
|
|
{25,26,27,28,29,30,31,32}, //segment enable pins of MM5450 (a,b,c,d,e,f,g,DP)
|
|
{17,18,19,20,21,22,23,24},
|
|
{9,10,11,12,13,14,15,16},
|
|
{1,2,3,4,5,6,7,8}
|
|
};
|
|
*/
|
|
|
|
|
|
byte sideEnablePins[] = {34, 33}; //enable pins for the two groups of 4 digits (2x4 LEDs)
|
|
// sideEnablePins[0] = MM5450 output 34 = enable line for "side 0" (digit groups 0,1)
|
|
// sideEnablePins[1] = MM5450 output 33 = enable line for "side 1" (digit groups 2,3)
|
|
// Only one side's enable line is driven HIGH at a time -> the two 4-digit
|
|
// groups share the same 32 segment lines and are multiplexed in time.
|
|
|
|
//MM5450 control pins (GPIO numbers - see clocks.h D5pin/D6pin/etc for the NodeMCU Dx aliases:
|
|
// GPIO14=D5, GPIO12=D6, GPIO13=D7, GPIO15=D8)
|
|
#define PIN_LE 14 // D5 Shift Register Latch Enable
|
|
#define PIN_CLK 12 // D6 Shift Register Clock
|
|
#define PIN_DATA 13 // D7 Shift Register Data
|
|
#define PIN_BR 15 // D8 Shift Register Brightness (chip-enable/blank output, active HIGH)
|
|
|
|
// Bitmasks for the same pins, used with WRITE_PERI_REG(PIN_OUT_SET/CLEAR, ...) for fast
|
|
// direct GPIO register access inside the ISR (digitalWrite() is too slow/has too much
|
|
// call overhead to meet the tight bit-banging timing below).
|
|
#define PIN_LE_BIT 1<<14 // D5 Shift Register Latch Enable
|
|
#define PIN_CLK_BIT 1<<12 // D6 Shift Register Clock
|
|
#define PIN_DATA_BIT 1<<13 // D7 Shift Register Data
|
|
#define PIN_BR_BIT 1<<15 // D8 Shift Register Brightness
|
|
|
|
//------------------abcdefgDP---------------- definition of different characters
|
|
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)
|
|
};
|
|
|
|
#define MAXCHARS sizeof(charDefinition) // (currently unused in this file - kept for reference/consistency with other drivers)
|
|
|
|
int PWMrefresh = 10000; //usec, Multiplex time period (one full ON+OFF brightness cycle). Greater value => slower multiplex/PWM frequency
|
|
// PWM_min/PWM_max bound the ON-time portion of PWMrefresh: since the MM5450 has no
|
|
// hardware intensity register, brightness is entirely software PWM - writeDisplay()
|
|
// re-arms the hw timer with a shorter or longer "ON" interval depending on the
|
|
// requested brightness (see PWMtimeBrightness below).
|
|
int PWM_min = 500;
|
|
int PWM_max = 10000;
|
|
//int PWMtiming[11] = {0,500,800,1200,2000,2500,3000,4500,6000,8000,10000};
|
|
|
|
// bitBuffer holds the full 34-bit MM5450 serial frame plus its leading start bit
|
|
// (bitBuffer[0] = start bit, bitBuffer[1..34] = one byte per output pin, 0/1 = LED off/on).
|
|
// One array element is used per bit (not packed) to make indexing by MM5450 pin number
|
|
// trivial for writeBits(); size 36 leaves a little headroom beyond the 35 bits actually sent.
|
|
byte bitBuffer[36];
|
|
|
|
void setup_pins() {
|
|
#if defined(ESP8266)
|
|
#else
|
|
#error "Only 8266 Board is supported!"
|
|
#endif
|
|
|
|
DPRINTLN("MM5450 - Setup pins...");
|
|
pinMode(PIN_LE, OUTPUT); regPin(PIN_LE, "PIN_LE");
|
|
pinMode(PIN_BR, OUTPUT); regPin(PIN_BR, "PIN_BR");
|
|
pinMode(PIN_DATA, OUTPUT); regPin(PIN_DATA, "PIN_DATA");
|
|
pinMode(PIN_CLK, OUTPUT); regPin(PIN_CLK, "PIN_CLK");
|
|
digitalWrite(PIN_BR, HIGH); //brightness
|
|
|
|
digitsOnly = false; // this driver's charDefinition[] also supports letters (A,P,C,deg,%,I,F) besides 0-9
|
|
startTimer(); // arms the periodic hw timer -> writeDisplay() becomes the multiplex/PWM ISR
|
|
|
|
}
|
|
|
|
// Bit-bangs one full 35-bit frame (start bit + 34 output bits) into the MM5450 shift
|
|
// register and latches it. Runs inside the timer ISR (called from writeDisplay()), so it
|
|
// uses direct GPIO SET/CLEAR peripheral-register writes instead of digitalWrite() - the
|
|
// latter's call/pin-lookup overhead would blow the tight clock pulse widths needed here.
|
|
void ICACHE_RAM_ATTR writeBits() {
|
|
//https://sub.nanona.fi/esp8266/timing-and-ticks.html
|
|
//One tick is 1us / 80 = 0.0125us = 12.5ns on 80MHz
|
|
//One tick is 1us / 80 = 0.0125us = 6.25ns on 160MHz
|
|
// 1 NOP is 1 tick
|
|
|
|
//noInterrupts(); // not needed: this code already runs inside the timer ISR itself
|
|
WRITE_PERI_REG( PIN_OUT_CLEAR, PIN_LE_BIT ); //DATA_ENABLE - LE must be LOW while shifting data in
|
|
for (int t = 0; t < 5; t++) asm volatile ("nop"); //100nsec
|
|
|
|
for (int i = 0; i < 36; i++) { // shift bitBuffer out LSB-index-first (index 0 = start bit, sent first)
|
|
if (bitBuffer[i]) {
|
|
WRITE_PERI_REG( PIN_OUT_SET, PIN_DATA_BIT ); //digitalWrite(PIN_DATA,HIGH);
|
|
}
|
|
else {
|
|
WRITE_PERI_REG( PIN_OUT_CLEAR, PIN_DATA_BIT ); //digitalWrite(PIN_DATA,LOW);
|
|
}
|
|
|
|
for (int t = 0; t < 12; t++) asm volatile ("nop"); //300nsec - data setup time before clock edge
|
|
|
|
WRITE_PERI_REG( PIN_OUT_SET, PIN_CLK_BIT ); // MM5450 samples DATA on this rising CLK edge
|
|
for (int t = 0; t < 12; t++) asm volatile ("nop"); //300nsec
|
|
WRITE_PERI_REG( PIN_OUT_CLEAR, PIN_CLK_BIT );
|
|
for (int t = 0; t < 12; t++) asm volatile ("nop"); //300nsec
|
|
} //end for
|
|
|
|
WRITE_PERI_REG( PIN_OUT_SET, PIN_LE_BIT ); // rising edge on LE latches the shifted-in frame to the outputs
|
|
//interrupts();
|
|
}
|
|
|
|
// Main multiplex/PWM ISR, attached to the ESP8266 hw timer1 by startTimer().
|
|
// timer1 is configured TIM_SINGLE (one-shot), so this handler must re-arm itself via
|
|
// timer1_write() at the very end of every call - the display refresh rate/PWM duty
|
|
// cycle is entirely driven by how long each re-armed interval is (see 'timer' below).
|
|
// Each call does ONE of: (a) blank the display for the computed OFF duration (brightness
|
|
// PWM low phase), or (b) blank for the ON duration but first shift out a fresh frame for
|
|
// whichever "side" (group of 4 digits) is up next, alternating sides on every ON call.
|
|
void ICACHE_RAM_ATTR writeDisplay() { // Writes to the MM5450 driver for LEDS
|
|
static volatile int timer = PWMrefresh;
|
|
static volatile boolean state = true; // true = ON phase of the PWM cycle, false = OFF phase
|
|
static volatile int brightness;
|
|
static int PWMtimeBrightness;
|
|
static volatile uint32_t val;
|
|
static volatile byte pos = 0;
|
|
static byte side = 0;
|
|
byte num = 0;
|
|
|
|
if (EEPROMsaving) { //stop refresh, while EEPROM write is in progress!
|
|
// Flash writes on ESP8266 briefly disable interrupts/stall the CPU; touching the
|
|
// display bus during that window could corrupt the SPI-like bit-bang timing, so we
|
|
// just keep re-arming the timer with a fixed period and skip the actual refresh.
|
|
timer1_write(PWMrefresh);
|
|
return;
|
|
}
|
|
|
|
intCounter++;
|
|
brightness = displayON ? prm.dayBright : prm.nightBright;
|
|
if (brightness > MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
|
|
|
|
if (autoBrightness && displayON) {
|
|
// Ambient-light (lx) driven brightness: brighter room -> longer ON time within PWMrefresh
|
|
PWMtimeBrightness = max(PWM_min, PWM_max * lx / MAXIMUM_LUX);
|
|
}
|
|
else
|
|
// Manual brightness setting (0..MAXBRIGHTNESS) mapped linearly onto the ON-time window
|
|
PWMtimeBrightness = max(PWM_min, PWM_max * brightness / MAXBRIGHTNESS);
|
|
|
|
if ((!autoBrightness) && (brightness == MAXBRIGHTNESS))
|
|
// At max manual brightness PWMtimeBrightness == PWMrefresh, so the OFF phase would be
|
|
// ~0us; forcing state=true here keeps the ISR permanently in the ON branch instead of
|
|
// wastefully toggling into a near-zero-length OFF phase every other call.
|
|
state = true;
|
|
|
|
if (state) { //ON state
|
|
timer = PWMtimeBrightness;
|
|
timerON = timer;
|
|
timerOFF = PWMrefresh - PWMtimeBrightness;
|
|
}
|
|
else { //OFF state
|
|
timer = PWMrefresh - PWMtimeBrightness;
|
|
}
|
|
if (timer < 500) timer = 500; //safety only... (avoid re-arming the hw timer with too short/zero an interval)
|
|
|
|
if ( (brightness == 0) || (!state) || (!radarON)) { //OFF state, blank digit
|
|
WRITE_PERI_REG( PIN_OUT_CLEAR, PIN_BR_BIT ); //OFF - BR pin blanks all MM5450 outputs regardless of shift-register content
|
|
// radarON reflects presence-detection (radar/PIR): when nobody is around the display
|
|
// is blanked here (power saving / tube-life) without touching the multiplex state.
|
|
}
|
|
else { //ON state
|
|
WRITE_PERI_REG( PIN_OUT_SET, PIN_BR_BIT ); //ON
|
|
|
|
memset(bitBuffer, 0, sizeof(bitBuffer)); //clear array
|
|
bitBuffer[0] = 1; //starting bit required to frame the 34 data bits (see MM5450 datasheet)
|
|
|
|
bitBuffer[sideEnablePins[side]] = 0;
|
|
writeBits(); // 1st transfer: push an all-blank frame (except start bit) so BOTH sides are
|
|
// momentarily off before the new side's segments are shifted in - avoids
|
|
// ghosting/cross-talk between the outgoing and incoming digit group.
|
|
for (int t = 0; t < 100; t++) asm volatile ("nop"); //clear display and wait between switching the two sides
|
|
bitBuffer[sideEnablePins[side]] = 0; //disable old side..
|
|
// NOTE: bitBuffer was just memset to 0 a few lines above and this index was already
|
|
// cleared before the previous writeBits() call, so this re-assignment is a redundant
|
|
// no-op (POSSIBLE dead/duplicated code - likely meant to clear something else, e.g.
|
|
// the *other* side, but 'side' has not been toggled yet at this point).
|
|
|
|
if (side == 0) side = 1; else side = 0; //change side...
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
pos = 7 - (4 * side + i);
|
|
// POSSIBLE BUG: pos always ranges 0..7 (8 digit positions) regardless of maxDigits
|
|
// (=6 above). digit[6] and digit[7] are never written by the core display-update
|
|
// logic when maxDigits==6, and being plain (non-initialized-to-BLANK) globals they
|
|
// default to 0 -> segment pattern for '0'. If the physical PCB for CLOCK_11 really
|
|
// only has 6 digits wired, positions 6/7 are presumably just unused MM5450 outputs
|
|
// (harmless); if it has 8 digits, the top 2 would never be updated by the clock logic.
|
|
num = digit[pos];
|
|
//if (animMask[pos]>0) val &= animationMaskBits[animMask[pos]-1]; //animationMode 6, mask characters from up to down and back
|
|
// (wipe/mask animation support - referenced 'val' and 'animationMaskBits' are unused/
|
|
// undefined in this file, so this feature is not implemented for the MM5450 driver)
|
|
for (int j = 0; j <= 7; j++) //from a to g
|
|
if ((charDefinition[num] & 1 << (7 - j)) != 0) {
|
|
bitBuffer[segmentEnablePins[i][j]] = 1;
|
|
}
|
|
if (digitDP[pos]) bitBuffer[segmentEnablePins[i][7]] = 1; //Decimal Point
|
|
} //end for
|
|
|
|
bitBuffer[sideEnablePins[side]] = 1; //enable new side
|
|
writeBits(); // 2nd transfer: actual segment data + new side's enable bit
|
|
} //end else ON
|
|
|
|
state = !state;
|
|
timer1_write(timer); // re-arm the one-shot hw timer for the next ON or OFF phase
|
|
}
|
|
|
|
|
|
// Both required by the common driver interface but intentionally no-ops here: this driver
|
|
// does all display updates from the writeDisplay() ISR (brightness PWM + side multiplex
|
|
// depend on continuous timer-driven refresh), so there is nothing useful for a one-shot
|
|
// blank (clearTubes) or a one-shot blocking full-frame write (writeDisplaySingle) to do -
|
|
// the next ISR tick will simply pick up the current digit[]/digitDP[] contents.
|
|
void clearTubes() {}
|
|
void writeDisplaySingle() {}
|
|
#endif
|