diff --git a/Gaugecontroller/Gaugecontroller.ino b/Gaugecontroller/Gaugecontroller.ino index 64a7111..8236568 100644 --- a/Gaugecontroller/Gaugecontroller.ino +++ b/Gaugecontroller/Gaugecontroller.ino @@ -1,4 +1,5 @@ #include +#include #include #include @@ -12,6 +13,212 @@ static const uint8_t LED_DATA_PIN = 22; #define DEBUG_PORT Serial1 static const unsigned long SERIAL_BAUD = 38400; +namespace vfd { + +constexpr uint8_t kDataPin = 46; +constexpr uint8_t kClockPin = 47; +constexpr uint8_t kLatchPin = 48; +constexpr int8_t kBlankPin = 49; // Set to -1 if BL/OE is not connected +constexpr bool kBlankActiveHigh = true; + +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 displayBuffer[kDigitCount] = {' ', ' ', ' ', ' '}; +bool pointEnabled = false; +bool bellEnabled = false; +uint8_t currentPhase = 0; + +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(kLatchPin, HIGH); + digitalWrite(kClockPin, HIGH); + + for (int8_t bit = kDriverBits - 1; bit >= 0; --bit) { + digitalWrite(kDataPin, (word >> bit) & 0x1U ? HIGH : LOW); + digitalWrite(kClockPin, LOW); + digitalWrite(kClockPin, HIGH); + } + + digitalWrite(kLatchPin, LOW); + digitalWrite(kLatchPin, HIGH); +} + +void setBlanked(bool blanked) { + if (kBlankPin < 0) return; + + const bool level = kBlankActiveHigh ? blanked : !blanked; + digitalWrite(kBlankPin, level ? HIGH : LOW); +} + +void writeText(const char* text) { + for (uint8_t i = 0; i < kDigitCount; ++i) { + 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) { + displayBuffer[start + i] = text[i]; + } +} + +void clear() { + writeText(""); + pointEnabled = false; + bellEnabled = false; +} + +bool parseCommand(const String& command) { + char displayText[16]; + size_t inputIndex = 0; + size_t displayIndex = 0; + + if (command.length() == 0) { + return false; + } + + if (command[inputIndex] == '-') { + if (displayIndex + 1 >= sizeof(displayText)) { + return false; + } + displayText[displayIndex++] = command[inputIndex++]; + } + + const size_t digitStart = inputIndex; + while (inputIndex < static_cast(command.length()) && + isxdigit(static_cast(command[inputIndex]))) { + if (displayIndex + 1 >= sizeof(displayText)) { + return false; + } + displayText[displayIndex++] = toupper(static_cast(command[inputIndex])); + ++inputIndex; + } + + if (inputIndex == digitStart) { + return false; + } + + bool newPointEnabled = false; + bool newBellEnabled = false; + while (inputIndex < static_cast(command.length())) { + if (command[inputIndex] == '.') { + newPointEnabled = true; + } else if (command[inputIndex] == '!') { + newBellEnabled = true; + } else { + return false; + } + ++inputIndex; + } + + displayText[displayIndex] = '\0'; + writeText(displayText); + pointEnabled = newPointEnabled; + bellEnabled = newBellEnabled; + return true; +} + +void renderDigit(uint8_t digitIndex) { + uint32_t word = 0; + const uint8_t segments = encodeCharacter(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 (pointEnabled) { + word |= 1UL << kPointSegmentBit; + } + if (bellEnabled) { + word |= 1UL << kBellSegmentBit; + } + shiftDriverWord(word); +} + +void begin() { + pinMode(kDataPin, OUTPUT); + pinMode(kClockPin, OUTPUT); + pinMode(kLatchPin, OUTPUT); + if (kBlankPin >= 0) { + pinMode(kBlankPin, OUTPUT); + } + + digitalWrite(kDataPin, LOW); + digitalWrite(kClockPin, HIGH); + digitalWrite(kLatchPin, HIGH); + setBlanked(true); + writeText("0"); + shiftDriverWord(0); +} + +void refresh() { + setBlanked(true); + if (currentPhase < kDigitCount) { + renderDigit(currentPhase); + } else if (pointEnabled || bellEnabled) { + renderIndicator(); + } else { + shiftDriverWord(0); + } + setBlanked(false); + delayMicroseconds(kDigitHoldMicros); + setBlanked(true); + + currentPhase = (currentPhase + 1) % (kDigitCount + 1); +} + +} // namespace vfd + struct GaugePins { uint8_t dirPin; uint8_t stepPin; @@ -111,7 +318,8 @@ bool ledsDirty = false; // LED // BLINK [ ] // BREATHE -// DFLASH +// DFLASH +// VFD // PING // // Controller -> host replies / events: @@ -136,6 +344,8 @@ bool ledsDirty = false; // Sent by LED/BLINK/BREATHE/DFLASH when an LED index or range is invalid. // ERR BAD_TIME // Sent by BLINK/BREATHE when the timing parameter is invalid. +// ERR BAD_VFD +// Sent by VFD when the text payload is malformed. // POS // Emitted once per gauge before the trailing OK reply to POS?. // LED @@ -594,6 +804,32 @@ bool parsePing(const String& line) { return false; } +// Parses `VFD ` where is up to four hex characters with optional `.` and `!` suffixes. +// Replies: `OK`, `ERR BAD_VFD`. +bool parseVfd(const String& line) { + if (line == "VFD") { + vfd::clear(); + sendReply("OK"); + return true; + } + + if (!line.startsWith("VFD ")) return false; + + const String payload = line.substring(4); + if (payload.length() == 0) { + vfd::clear(); + sendReply("OK"); + return true; + } + + if (vfd::parseCommand(payload)) { + sendReply("OK"); + } else { + sendReply("ERR BAD_VFD"); + } + return true; +} + // Answers `LED?` with the current RGB values for every configured LED. // Emits one `LED ` line per configured LED, then replies `OK`. bool parseLedQuery(const String& line) { @@ -827,6 +1063,7 @@ void processLine(const String& line) { if (parseBlink(line)) return; if (parseBreathe(line)) return; if (parseDflash(line)) return; + if (parseVfd(line)) return; if (parsePing(line)) return; sendReply("ERR BAD_CMD"); @@ -885,6 +1122,8 @@ void setup() { FastLED.setBrightness(255); FastLED.show(); + vfd::begin(); + requestHomeAll(); DEBUG_PORT.println("READY"); @@ -895,6 +1134,7 @@ void setup() { // Main service loop: ingest commands, advance effects, move gauges, flush LEDs. void loop() { readCommands(); + vfd::refresh(); updateBlink(); for (uint8_t i = 0; i < GAUGE_COUNT; i++) { diff --git a/VFDStandalone/Pinout.md b/VFDStandalone/Pinout.md new file mode 100644 index 0000000..5d66ef4 --- /dev/null +++ b/VFDStandalone/Pinout.md @@ -0,0 +1,56 @@ +# Pinout + +This project uses an Arduino Mega 2560 with an `HV5812P` high-voltage shift register / latch driver. + +The sketch in [VFDStandalone.ino](/home/adebaumann/development/arduino_gauge_controller/VFDStandalone/VFDStandalone.ino:1) currently expects these logic connections. + +## Arduino Mega 2560 -> HV5812P + +| Mega Pin | Mega Function | HV5812P Signal | Notes | +|---|---|---|---| +| `D51` | `MOSI` | `DATA` / `DIN` | Serial data into the HV5812P | +| `D52` | `SCK` | `CLOCK` / `CLK` | Shift clock | +| `D53` | `SS` | `LATCH` / `STROBE` | Transfers shifted bits to the outputs | +| `D49` | GPIO | `BLANK` / `OE` | Optional. Set `kHvBlankPin = -1` in the sketch if unused | +| `GND` | Ground | Logic `GND` | Mega and HV5812P logic ground must be common | + +## HV5812P Outputs -> VFD Tube + +| HV5812P Output | Function | +|---|---| +| `HVOut1` | Segment `A` | +| `HVOut2` | Segment `B` | +| `HVOut3` | Segment `C` | +| `HVOut4` | Segment `D` | +| `HVOut5` | Segment `E` | +| `HVOut6` | Segment `F` | +| `HVOut7` | Segment `G` | +| `HVOut8` | Decimal point segment | +| `HVOut9` | Alarm bell segment | +| `HVOut10` | Digit grid 1 | +| `HVOut11` | Digit grid 2 | +| `HVOut12` | Digit grid 3 | +| `HVOut13` | Digit grid 4 | +| `HVOut14` | Indicator grid between digits 2 and 3 | + +## Serial Input Format + +Examples supported by the sketch: + +- `1234` -> digits only +- `1234.` -> decimal point on +- `1234!` -> alarm bell on +- `1234.!` -> decimal point and alarm bell on + +## Power and Safety Notes + +- The Arduino `5V` pin is for the logic side only. +- The HV5812P also needs its required logic supply and high-voltage supply per the datasheet. +- The VFD filament, grid, and segment high-voltage wiring are separate from the Arduino logic pins. +- Do not connect any high-voltage VFD node directly to the Arduino Mega. +- If the blanking behavior is inverted on your board, change `kBlankActiveHigh` in the sketch. + +## Important + +This file names the functional signals on the `HV5812P`, not the package pin numbers. +If you want a package-pin wiring table too, I can add one once you confirm the exact datasheet variant / package orientation you are using.