diff --git a/VFDStandalone/VFDStandalone.ino b/VFDStandalone/VFDStandalone.ino new file mode 100644 index 0000000..d79218e --- /dev/null +++ b/VFDStandalone/VFDStandalone.ino @@ -0,0 +1,342 @@ +// Arduino Mega 2560 + HV5812P VFD driver +// +// Tube wiring: +// - HVOut1..HVOut7 -> digit segments A..G +// - HVOut8 -> decimal point segment on the indicator grid +// - HVOut9 -> alarm bell segment on the indicator grid +// - HVOut10..HVOut13 -> digits 1..4 +// - HVOut14 -> indicator grid between digits 2 and 3 +// +// Send an integer over the USB serial port and it will be shown on the VFD. +// Examples: +// 42 +// -17 +// 1234. // enables the decimal point +// 1234! // enables the alarm bell +// 1234.! // enables both + +#include + +namespace { + +constexpr uint8_t kHvDataPin = 51; // MOSI on Mega 2560 +constexpr uint8_t kHvClockPin = 52; // SCK on Mega 2560 +constexpr uint8_t kHvLatchPin = 53; // User-configurable latch/strobe pin +constexpr int8_t kHvBlankPin = 49; // Set to -1 if BL/OE is not connected +constexpr bool kBlankActiveHigh = true; + +constexpr unsigned long kSerialBaud = 115200; +constexpr unsigned long kDigitHoldMicros = 2000; + +constexpr uint8_t kDigitCount = 4; +constexpr uint8_t kSegmentCount = 7; +constexpr uint8_t kDriverBits = 20; + +constexpr uint8_t kSegmentStartBit = 0; // HVOut1 -> bit 0 +constexpr uint8_t kPointSegmentBit = 7; // HVOut8 -> bit 7 +constexpr uint8_t kBellSegmentBit = 8; // HVOut9 -> bit 8 +constexpr uint8_t kGridStartBit = 9; // HVOut10 -> bit 9 +constexpr uint8_t kIndicatorGridBit = 13; // HVOut14 -> bit 13 + +char g_displayBuffer[kDigitCount] = {' ', ' ', ' ', ' '}; +char g_inputBuffer[16]; +uint8_t g_inputLength = 0; +bool g_pointEnabled = false; +bool g_bellEnabled = false; +uint8_t g_rawOutput = 0; + +// Seven-segment encoding order is A, B, C, D, E, F, G. +uint8_t encodeCharacter(char c) { + switch (c) { + case '0': return 0b0111111; + case '1': return 0b0000110; + case '2': return 0b1011011; + case '3': return 0b1001111; + case '4': return 0b1100110; + case '5': return 0b1101101; + case '6': return 0b1111101; + case '7': return 0b0000111; + case '8': return 0b1111111; + case '9': return 0b1101111; + case 'A': + case 'a': return 0b1110111; + case 'B': + case 'b': return 0b1111100; + case 'C': + case 'c': return 0b0111001; + case 'D': + case 'd': return 0b1011110; + case 'E': + case 'e': return 0b1111001; + case 'F': + case 'f': return 0b1110001; + case '-': return 0b1000000; + default: return 0; + } +} + +void shiftDriverWord(uint32_t word) { + digitalWrite(kHvLatchPin, HIGH); + digitalWrite(kHvClockPin, HIGH); + + for (int8_t bit = kDriverBits - 1; bit >= 0; --bit) { + digitalWrite(kHvDataPin, (word >> bit) & 0x1U ? HIGH : LOW); + digitalWrite(kHvClockPin, LOW); + digitalWrite(kHvClockPin, HIGH); + } + + digitalWrite(kHvLatchPin, LOW); + digitalWrite(kHvLatchPin, HIGH); +} + +void setDisplayBlanked(bool blanked) { + if (kHvBlankPin < 0) { + return; + } + + const bool level = kBlankActiveHigh ? blanked : !blanked; + digitalWrite(kHvBlankPin, level ? HIGH : LOW); +} + +void blankDisplay() { + shiftDriverWord(0); +} + +uint32_t maskForHvOutput(uint8_t hvOutput) { + if (hvOutput == 0 || hvOutput > kDriverBits) { + return 0; + } + + return 1UL << (hvOutput - 1); +} + +void renderDigit(uint8_t digitIndex) { + uint32_t word = 0; + const uint8_t segments = encodeCharacter(g_displayBuffer[digitIndex]); + + for (uint8_t segment = 0; segment < kSegmentCount; ++segment) { + if ((segments >> segment) & 0x1U) { + word |= (1UL << (kSegmentStartBit + segment)); + } + } + + word |= (1UL << (kGridStartBit + digitIndex)); + shiftDriverWord(word); +} + +void renderIndicator() { + uint32_t word = 1UL << kIndicatorGridBit; + + if (g_pointEnabled) { + word |= 1UL << kPointSegmentBit; + } + + if (g_bellEnabled) { + word |= 1UL << kBellSegmentBit; + } + + shiftDriverWord(word); +} + +void writeTextToDisplay(const char* text) { + for (uint8_t i = 0; i < kDigitCount; ++i) { + g_displayBuffer[i] = ' '; + } + + size_t len = strlen(text); + if (len > kDigitCount) { + text += len - kDigitCount; + len = kDigitCount; + } + + const uint8_t start = kDigitCount - len; + for (uint8_t i = 0; i < len; ++i) { + g_displayBuffer[start + i] = text[i]; + } +} + +void setDisplayFromNumber(long value) { + char buffer[16]; + ltoa(value, buffer, 10); + writeTextToDisplay(buffer); +} + +bool parseDisplayCommand(const char* input, + char* displayText, + size_t displayTextSize, + bool& pointEnabled, + bool& bellEnabled) { + size_t inputIndex = 0; + size_t displayIndex = 0; + + if (input[inputIndex] == '-') { + if (displayIndex + 1 >= displayTextSize) { + return false; + } + displayText[displayIndex++] = input[inputIndex++]; + } + + const size_t digitStart = inputIndex; + while (isxdigit(static_cast(input[inputIndex]))) { + if (displayIndex + 1 >= displayTextSize) { + return false; + } + displayText[displayIndex] = toupper(static_cast(input[inputIndex])); + ++displayIndex; + ++inputIndex; + } + + if (inputIndex == digitStart) { + return false; + } + + pointEnabled = false; + bellEnabled = false; + + while (input[inputIndex] != '\0') { + if (input[inputIndex] == '.') { + pointEnabled = true; + } else if (input[inputIndex] == '!') { + bellEnabled = true; + } else { + return false; + } + ++inputIndex; + } + + displayText[displayIndex] = '\0'; + return true; +} + +bool parseRawOutputCommand(const char* input, uint8_t& hvOutput) { + if (strncmp(input, "RAW ", 4) != 0) { + return false; + } + + char* endPtr = nullptr; + const long parsed = strtol(input + 4, &endPtr, 10); + if (*endPtr != '\0' || parsed < 0 || parsed > kDriverBits) { + return false; + } + + hvOutput = static_cast(parsed); + return true; +} + +void commitSerialBuffer() { + if (g_inputLength == 0) { + return; + } + + g_inputBuffer[g_inputLength] = '\0'; + + uint8_t rawOutput = 0; + if (parseRawOutputCommand(g_inputBuffer, rawOutput)) { + g_rawOutput = rawOutput; + if (g_rawOutput == 0) { + Serial.println(F("RAW mode OFF")); + } else { + Serial.print(F("RAW mode: HVOUT")); + Serial.println(g_rawOutput); + } + g_inputLength = 0; + return; + } + + char displayText[16]; + bool pointEnabled = false; + bool bellEnabled = false; + if (parseDisplayCommand(g_inputBuffer, displayText, sizeof(displayText), pointEnabled, bellEnabled)) { + g_rawOutput = 0; + writeTextToDisplay(displayText); + g_pointEnabled = pointEnabled; + g_bellEnabled = bellEnabled; + Serial.print(F("Displaying: ")); + Serial.println(displayText); + Serial.print(F("Point: ")); + Serial.println(g_pointEnabled ? F("ON") : F("OFF")); + Serial.print(F("Bell: ")); + Serial.println(g_bellEnabled ? F("ON") : F("OFF")); + } else { + Serial.print(F("Ignored invalid input: ")); + Serial.println(g_inputBuffer); + } + + g_inputLength = 0; +} + +void pollSerial() { + while (Serial.available() > 0) { + const char incoming = static_cast(Serial.read()); + + if (incoming == '\r' || incoming == '\n') { + commitSerialBuffer(); + continue; + } + + if (incoming == '\b' || incoming == 127) { + if (g_inputLength > 0) { + --g_inputLength; + } + continue; + } + + if (g_inputLength < sizeof(g_inputBuffer) - 1) { + g_inputBuffer[g_inputLength++] = incoming; + } + } +} + +void refreshDisplay() { + if (g_rawOutput != 0) { + setDisplayBlanked(true); + shiftDriverWord(maskForHvOutput(g_rawOutput)); + setDisplayBlanked(false); + delayMicroseconds(kDigitHoldMicros); + return; + } + + static uint8_t currentPhase = 0; + + setDisplayBlanked(true); + if (currentPhase < kDigitCount) { + renderDigit(currentPhase); + } else if (g_pointEnabled || g_bellEnabled) { + renderIndicator(); + } else { + blankDisplay(); + } + setDisplayBlanked(false); + delayMicroseconds(kDigitHoldMicros); + setDisplayBlanked(true); + + currentPhase = (currentPhase + 1) % (kDigitCount + 1); +} + +} // namespace + +void setup() { + pinMode(kHvDataPin, OUTPUT); + pinMode(kHvClockPin, OUTPUT); + pinMode(kHvLatchPin, OUTPUT); + if (kHvBlankPin >= 0) { + pinMode(kHvBlankPin, OUTPUT); + } + + digitalWrite(kHvDataPin, LOW); + digitalWrite(kHvClockPin, HIGH); + digitalWrite(kHvLatchPin, HIGH); + setDisplayBlanked(true); + + Serial.begin(kSerialBaud); + writeTextToDisplay("0"); + blankDisplay(); + + Serial.println(F("HV5812P VFD controller ready.")); + Serial.println(F("Send an integer followed by newline.")); +} + +void loop() { + pollSerial(); + refreshDisplay(); +}