diff --git a/.github/workflows/io-bords.yml b/.github/workflows/io-boards.yml similarity index 100% rename from .github/workflows/io-bords.yml rename to .github/workflows/io-boards.yml diff --git a/.gitignore b/.gitignore index bf10959..155326f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea +.vscode .pio -.DS_Store \ No newline at end of file +.DS_Store +*.pio.h diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json deleted file mode 100644 index 1fb4880..0000000 --- a/.vscode/c_cpp_properties.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "configurations": [ - { - "name": "Mac", - "includePath": [ - "${workspaceFolder}/**", - "${env:HOME}/.platformio/packages/framework-arduinopico/**" - ], - "defines": [], - "intelliSenseMode": "macos-clang-arm64" - } - ], - "version": 4 -} \ No newline at end of file diff --git a/src/EffectsController.cpp b/src/EffectsController.cpp index 169deb0..218e029 100644 --- a/src/EffectsController.cpp +++ b/src/EffectsController.cpp @@ -83,10 +83,6 @@ void EffectsController::addEffect(EffectContainer *container) { stackEffectContainers[++stackCounter] = container; } -void EffectsController::attachBrightnessControl(byte port, byte poti) { - brightnessControl[--port] = poti; -} - void EffectsController::setBrightness(byte port, byte brightness) { ws2812FXDevices[--port][0]->setBrightness(brightness); ws2812FXbrightness[port] = brightness; @@ -142,6 +138,12 @@ void EffectsController::handleEvent(Event *event) { void EffectsController::handleEvent(ConfigEvent *event) { if (event->boardId == boardId) { + if (flickerState) + _ledBuiltInDevice->on(); + else + _ledBuiltInDevice->off(); + flickerState = !flickerState; + switch (event->topic) { case CONFIG_TOPIC_PLATFORM: platform = event->value; @@ -517,24 +519,6 @@ void EffectsController::update() { } } } - - if (brightnessControlBasePin > 0) { - if (millis() - brightnessUpdateInterval > - UPDATE_INTERVAL_WS2812FX_BRIGHTNESS) { - // Don't update the brightness too often. - brightnessUpdateInterval = millis(); - for (byte i = 0; i < PPUC_MAX_BRIGHTNESS_CONTROLS; i++) { - brightnessControlReads[i] = - analogRead(brightnessControlBasePin + i) / 4; - } - for (byte i = 0; i < PPUC_MAX_WS2812FX_DEVICES; i++) { - if (brightnessControl[i] > 0) { - setBrightness(i + 1, - brightnessControlReads[brightnessControl[i - 1]]); - } - } - } - } } void EffectsController::start() { diff --git a/src/EffectsController.h b/src/EffectsController.h index 280997b..f27b579 100644 --- a/src/EffectsController.h +++ b/src/EffectsController.h @@ -59,7 +59,7 @@ class EffectsController : public EventListener { if (controllerType == CONTROLLER_16_8_1) { // Read bordID. Ideal value at 10bit resolution: (DIP+1)*1023*2/35 // -> 58.46 to 935.3 - boardId = 16 - ((int)((analogRead(28) + 29.23) / 58.46)); + boardId = (16 - ((int)((analogRead(28) + 29.23) / 58.46))) & 0b0111; _ledBuiltInDevice = new LedBuiltInDevice(); _ledBuiltInDevice->on(); @@ -133,10 +133,7 @@ class EffectsController : public EventListener { byte ws2812FXbrightness[PPUC_MAX_WS2812FX_DEVICES] = {0}; EffectContainer* stackEffectContainers[EFFECT_STACK_SIZE]; int stackCounter = -1; - byte brightnessControl[PPUC_MAX_WS2812FX_DEVICES] = {0}; - byte brightnessControlReads[PPUC_MAX_BRIGHTNESS_CONTROLS] = {0}; - byte brightnessControlBasePin = 0; - + bool flickerState = false; int mode = 0; byte platform; diff --git a/src/EventDispatcher/CrossLinkDebugger.cpp b/src/EventDispatcher/CrossLinkDebugger.cpp index b731403..6a7c287 100644 --- a/src/EventDispatcher/CrossLinkDebugger.cpp +++ b/src/EventDispatcher/CrossLinkDebugger.cpp @@ -9,7 +9,7 @@ CrossLinkDebugger::CrossLinkDebugger() { Serial.print("PPUC board #"); // Read bordID. Ideal value at 10bit resolution: (DIP+1)*1023*2/35 -> 58.46 // to 935.3 - Serial.println(16 - ((int)((analogRead(28) + 29.23) / 58.46))); + Serial.println((16 - ((int)((analogRead(28) + 29.23) / 58.46))) & 0b0111); Serial.println("PPUC core #0 started"); Serial.println("PPUC CrossLinkDebugger"); Serial.println("----------------------"); diff --git a/src/EventDispatcher/Event.h b/src/EventDispatcher/Event.h index 56381b6..b024f1d 100644 --- a/src/EventDispatcher/Event.h +++ b/src/EventDispatcher/Event.h @@ -48,6 +48,7 @@ #define CONFIG_TOPIC_SWITCHES 115 // "s" #define CONFIG_TOPIC_TRIGGER 116 // "t" #define CONFIG_TOPIC_SWITCH_MATRIX 120 // "x" +#define CONFIG_TOPIC_SWITCH_CHAIN 121 // "y" #define CONFIG_TOPIC_HOLD_POWER_ACTIVATION_TIME 65 // "A" #define CONFIG_TOPIC_DURATION 65 // "A" @@ -63,8 +64,10 @@ #define CONFIG_TOPIC_MIN_PULSE_TIME 77 // "M" #define CONFIG_TOPIC_FROM 77 // "M" #define CONFIG_TOPIC_MIN_INTENSITY 77 // "M" +#define CONFIG_TOPIC_DEBOUNCE_TIME 77 // "M" #define CONFIG_TOPIC_NUMBER 78 // "N" #define CONFIG_TOPIC_AMOUNT_LEDS 79 // "O" +#define CONFIG_TOPIC_NUM_ROWS 79 // "O" #define CONFIG_TOPIC_PORT 80 // "P" #define CONFIG_TOPIC_SPEED 83 // "S" #define CONFIG_TOPIC_SOURCE 83 // "S" @@ -74,6 +77,7 @@ #define CONFIG_TOPIC_LIGHT_UP 85 // "U" #define CONFIG_TOPIC_ACTIVE_LOW 86 // "V" #define CONFIG_TOPIC_POWER 87 // "W" +#define CONFIG_TOPIC_NEXT_BOARD 88 // "X" #define CONFIG_TOPIC_TYPE 89 // "Y" #define CONFIG_TOPIC_EFFECT 89 // "Y" #define CONFIG_TOPIC_MODE 90 // "Z" @@ -91,9 +95,6 @@ #define LED_TYPE_FLASHER 2 // Flasher #define LED_TYPE_LAMP 3 // Lamp -#define MATRIX_TYPE_COLUMN 1 // Column -#define MATRIX_TYPE_ROW 2 // Row - #define PWM_EFFECT_SINE 1 #define PWM_EFFECT_RAMP_DOWN_STOP 2 #define PWM_EFFECT_IMPULSE 3 diff --git a/src/EventDispatcher/EventDispatcher.cpp b/src/EventDispatcher/EventDispatcher.cpp index f2a6b03..50bfe26 100644 --- a/src/EventDispatcher/EventDispatcher.cpp +++ b/src/EventDispatcher/EventDispatcher.cpp @@ -1,6 +1,23 @@ #include "EventDispatcher.h" -EventDispatcher::EventDispatcher() {} +#include "hardware/uart.h" +#include + +namespace { +constexpr uint32_t kV2RxTimeoutUs = 8000; +} + +EventDispatcher::EventDispatcher() { + for (uint16_t i = 0; i < ppuc::v2::kMaxCoilBits; ++i) { + coilIndexToNumber[i] = i; + } + for (uint16_t i = 0; i < ppuc::v2::kMaxLampBits; ++i) { + lampIndexToNumber[i] = i; + } + for (uint16_t i = 0; i < ppuc::v2::kMaxSwitchBits; ++i) { + switchIndexToNumber[i] = i; + } +} void EventDispatcher::setRS485ModePin(int pin) { rs485 = true; @@ -22,6 +39,12 @@ void EventDispatcher::setCrossLinkSerial(HardwareSerial &reference) { hwSerial = (HardwareSerial *)&reference; } +void EventDispatcher::setDebug(bool enabled) { debugEnabled = enabled; } + +void EventDispatcher::setNextSwitchBoard(byte boardId) { + nextSwitchBoard = boardId; +} + void EventDispatcher::addListener(EventListener *eventListener) { addListener(eventListener, EVENT_SOURCE_ANY); } @@ -36,28 +59,22 @@ void EventDispatcher::addListener(EventListener *eventListener, char sourceId) { void EventDispatcher::dispatch(Event *event) { if (EVENT_RESET == event->sourceId) { // Force immediate handling of the reset event. Forget about the others. - for (int i = 0; i <= stackCounter; i++) { - if (stackEvents[i]) { - delete stackEvents[i]; - } + while (!eventQueue.empty()) { + Event *e = eventQueue.front(); + eventQueue.pop(); + delete e; } - stackCounter = -1; } - if (stackCounter < (EVENT_STACK_SIZE - 1)) { - stackEvents[++stackCounter] = event; + eventQueue.push(event); - if (event->localFast) { - for (byte i = 0; i <= numListeners; i++) { - if (event->sourceId == eventListenerFilters[i] || - EVENT_SOURCE_ANY == eventListenerFilters[i]) { - eventListeners[i]->handleEvent(event); - } + if (event->localFast) { + for (byte i = 0; i <= numListeners; i++) { + if (event->sourceId == eventListenerFilters[i] || + EVENT_SOURCE_ANY == eventListenerFilters[i]) { + eventListeners[i]->handleEvent(event); } } - } else { - // Too many events stacked, delete the event and free the memory. - delete event; } } @@ -88,6 +105,10 @@ void EventDispatcher::callListeners(Event *event, bool sendToOtherCore, multiCoreCrossLink->pushEvent(event); } + if (event->sourceId == EVENT_SOURCE_SWITCH) { + updateSwitchBitmap(event); + } + // delete the event and free the memory delete event; } @@ -108,149 +129,479 @@ void EventDispatcher::callListeners(ConfigEvent *event, bool sendToOtherCore) { delete event; } -void EventDispatcher::update() { - if (!rs485) { // We're on Core1, the EffectController. Transmit stacked - // events to Core0. - for (int i = 0; i <= stackCounter; i++) { - Event *event = stackEvents[i]; - callListeners(event, true, false); +bool EventDispatcher::readBytes(byte *buffer, size_t len) { + size_t offset = 0; + uint32_t start = micros(); + while (offset < len) { + if (hwSerial->available() > 0) { + buffer[offset++] = hwSerial->read(); + continue; } - // -1 means empty. - stackCounter = -1; - } else { - if (hwSerial->available() >= 7) { - bool success = false; - - byte startByte = hwSerial->read(); - if (startByte == 255) { - byte sourceId = hwSerial->read(); - if (sourceId != 0) { - if (sourceId == EVENT_CONFIGURATION) { - // Config Event has 12 bytes, 2 bytes are already parsed above. - while (hwSerial->available() < 10) { - } - - // We have a ConfigEvent. - byte boardId = hwSerial->read(); - byte topic = hwSerial->read(); - byte index = hwSerial->read(); - byte key = hwSerial->read(); - uint32_t value = (((uint32_t)hwSerial->read()) << 24) + - (((uint32_t)hwSerial->read()) << 16) + - (((uint32_t)hwSerial->read()) << 8) + - hwSerial->read(); - byte stopByte = hwSerial->read(); - if (stopByte == 0b10101010) { - stopByte = hwSerial->read(); - if (stopByte == 0b01010101) { - success = true; - callListeners( - new ConfigEvent(boardId, topic, index, key, value), true); - } - } - } else { - word eventId = word(hwSerial->read(), hwSerial->read()); - if (eventId != 0) { - byte value = hwSerial->read(); - byte stopByte = hwSerial->read(); - if (stopByte == 0b10101010) { - stopByte = hwSerial->read(); - if (stopByte == 0b01010101) { - success = true; - callListeners(new Event((char)sourceId, eventId, value), true, - false); - - if (sourceId == EVENT_POLL_EVENTS && board == value) { - digitalWrite(rs485Pin, HIGH); // Write. - // Wait until the RS485 converter switched to write mode. - delayMicroseconds(RS485_MODE_SWITCH_DELAY); - - for (int k = 0; k <= stackCounter; k++) { - Event *event = stackEvents[k]; - callListeners(event, true, true); - } - // -1 means empty. - stackCounter = -1; - - // Send NULL event to indicate that transmission is - // complete. - callListeners(new Event(EVENT_NULL, 1, board), false, true); - - lastPoll = millis(); - - // Flush the serial buffer and wait until done. - hwSerial->flush(); - digitalWrite(rs485Pin, LOW); // Read. - // Wait until the RS485 converter switched back to read - // mode. - delayMicroseconds(RS485_MODE_SWITCH_DELAY); - } else if (sourceId == EVENT_RUN) { - running = true; - } - - } else { - if (Serial) { - rp2040.idleOtherCore(); - Serial.print("Received wrong second stop byte "); - Serial.println(stopByte, DEC); - rp2040.resumeOtherCore(); - } - } - } else { - if (Serial) { - rp2040.idleOtherCore(); - Serial.print("Received wrong first stop byte "); - Serial.println(stopByte, DEC); - rp2040.resumeOtherCore(); - } - } - } else { - if (Serial) { - rp2040.idleOtherCore(); - Serial.print("Received invalid event id "); - Serial.println(eventId, DEC); - rp2040.resumeOtherCore(); - } - } - } + + if ((micros() - start) > 8000) { + return false; + } + } + + return true; +} + +size_t EventDispatcher::getV2PayloadBytes(ppuc::v2::FrameType frameType) { + switch (frameType) { + case ppuc::v2::kFrameSetup: + return ppuc::v2::kSetupPayloadBytes; + case ppuc::v2::kFrameMapping: + return ppuc::v2::kMappingPayloadBytes; + case ppuc::v2::kFrameConfig: + return ppuc::v2::kConfigPayloadBytes; + case ppuc::v2::kFrameOutputState: + return ppuc::v2::BitsToBytes(runtimeConfig.coilBits) + + ppuc::v2::BitsToBytes(runtimeConfig.lampBits); + case ppuc::v2::kFrameSwitchState: + return ppuc::v2::BitsToBytes(runtimeConfig.switchBits); + case ppuc::v2::kFrameSwitchNoChange: + return 0; + case ppuc::v2::kFrameHeartbeat: + case ppuc::v2::kFrameError: + case ppuc::v2::kFrameReset: + return 0; + default: + return 0; + } +} + +bool EventDispatcher::processV2Frame(const byte* frame, size_t payloadBytes) { + const ppuc::v2::FrameType frameType = ppuc::v2::ExtractType(frame[1]); + const size_t crcOffset = ppuc::v2::kHeaderBytes + payloadBytes; + uint16_t receivedCrc = word(frame[crcOffset], frame[crcOffset + 1]); + uint16_t expectedCrc = + ppuc::v2::Crc16Ccitt(frame, ppuc::v2::kHeaderBytes + payloadBytes); + if (receivedCrc != expectedCrc) { + v2RxCrcFail++; + return false; + } + v2RxFrames++; + + if (frameType == ppuc::v2::kFrameSetup) { + ppuc::v2::RuntimeConfig newConfig; + newConfig.coilBits = word(frame[4], frame[5]); + newConfig.lampBits = word(frame[6], frame[7]); + newConfig.switchBits = word(frame[8], frame[9]); + if (ppuc::v2::IsValidRuntimeConfig(newConfig)) { + runtimeConfig = newConfig; + memset(outputCoils, 0, sizeof(outputCoils)); + memset(outputLamps, 0, sizeof(outputLamps)); + memset(switchStates, 0, sizeof(switchStates)); + for (uint16_t i = 0; i < runtimeConfig.coilBits; ++i) { + coilIndexToNumber[i] = i; + } + for (uint16_t i = 0; i < runtimeConfig.lampBits; ++i) { + lampIndexToNumber[i] = i; + } + for (uint16_t i = 0; i < runtimeConfig.switchBits; ++i) { + switchIndexToNumber[i] = i; + } + if (!v2UartDmaActive) { + if (startV2UartDmaTransport()) { + v2CutoverOk++; } else { - if (Serial) { - rp2040.idleOtherCore(); - Serial.print("Received invalid source id "); - Serial.println(sourceId, DEC); - rp2040.resumeOtherCore(); - } + v2CutoverFail++; } - } else { - if (Serial) { - rp2040.idleOtherCore(); - Serial.print("Received wrong start byte "); - Serial.println(startByte, DEC); - rp2040.resumeOtherCore(); - } - // We didn't receive a start byte. Fake "success" to start over with the - // next byte. - success = true; } + } + return true; + } - if (success) { - if (error) { - error = false; - dispatch(new Event(EVENT_NO_ERROR, 1, board)); - } + if (frameType == ppuc::v2::kFrameMapping) { + const uint8_t domain = frame[4]; + const uint16_t index = word(frame[6], frame[7]); + const uint16_t number = word(frame[8], frame[9]); + + if (domain == ppuc::v2::kDomainCoil && index < runtimeConfig.coilBits) { + coilIndexToNumber[index] = number; + } else if (domain == ppuc::v2::kDomainLamp && + index < runtimeConfig.lampBits) { + lampIndexToNumber[index] = number; + } else if (domain == ppuc::v2::kDomainSwitch && + index < runtimeConfig.switchBits) { + switchIndexToNumber[index] = number; + } + return true; + } + + if (frameType == ppuc::v2::kFrameOutputState) { + const size_t coilBytes = ppuc::v2::BitsToBytes(runtimeConfig.coilBits); + const size_t lampBytes = ppuc::v2::BitsToBytes(runtimeConfig.lampBits); + applyOutputStates(&frame[4], coilBytes, &frame[4 + coilBytes], lampBytes); + if (frame[2] == board) { + if (switchDirty) { + sendSwitchStateFrame(nextSwitchBoard); + switchDirty = false; } else { - error = true; - dispatch(new Event(EVENT_ERROR, 1, board)); - - while (hwSerial->available()) { - byte bits = hwSerial->read(); - if (bits == 0b10101010 && hwSerial->available()) { - bits = hwSerial->read(); - if (bits == 0b01010101) { - // Now we should be back in sync. - break; - } + sendSwitchNoChangeFrame(nextSwitchBoard); + } + } + return true; + } + + if (frameType == ppuc::v2::kFrameSwitchState) { + const size_t switchBytes = ppuc::v2::BitsToBytes(runtimeConfig.switchBits); + applySwitchStates(&frame[4], switchBytes); + return true; + } + + if (frameType == ppuc::v2::kFrameSwitchNoChange) { + return true; + } + + if (frameType == ppuc::v2::kFrameReset) { + dispatch(new Event(EVENT_RESET)); + return true; + } + + if (frameType == ppuc::v2::kFrameConfig) { + callListeners( + new ConfigEvent(frame[4], frame[5], frame[6], frame[7], + (((uint32_t)frame[8]) << 24) | + (((uint32_t)frame[9]) << 16) | + (((uint32_t)frame[10]) << 8) | + ((uint32_t)frame[11])), + true); + return true; + } + + return true; +} + +bool EventDispatcher::startV2UartDmaTransport() { + if (v2UartDmaActive) { + return true; + } + + int rxDma = dma_claim_unused_channel(false); + if (rxDma < 0) { + return false; + } + + int txDma = dma_claim_unused_channel(false); + if (txDma < 0) { + dma_channel_unclaim(rxDma); + return false; + } + v2RxDmaChannel = rxDma; + v2TxDmaChannel = txDma; + v2UartDmaActive = true; + v2RxState = V2_RX_IDLE; + v2RxPayloadBytes = 0; + v2RxStateStartUs = micros(); + + return true; +} + +void EventDispatcher::stopV2UartDmaTransport() { + if (!v2UartDmaActive) { + return; + } + + dma_channel_abort(v2RxDmaChannel); + dma_channel_abort(v2TxDmaChannel); + dma_channel_unclaim(v2RxDmaChannel); + dma_channel_unclaim(v2TxDmaChannel); + + v2UartDmaActive = false; + v2RxState = V2_RX_IDLE; + v2RxDmaChannel = -1; + v2TxDmaChannel = -1; +} + +bool EventDispatcher::sendV2FrameUartDma(const byte* frame, size_t frameBytes) { + if (!v2UartDmaActive || !frame || frameBytes == 0) { + return false; + } + + memcpy(v2DmaTxBuffer, frame, frameBytes); + dma_channel_config txConfig = dma_channel_get_default_config(v2TxDmaChannel); + channel_config_set_transfer_data_size(&txConfig, DMA_SIZE_8); + channel_config_set_dreq(&txConfig, uart_get_dreq(uart1, true)); + channel_config_set_read_increment(&txConfig, true); + channel_config_set_write_increment(&txConfig, false); + + digitalWrite(rs485Pin, HIGH); // Write. + delayMicroseconds(RS485_MODE_SWITCH_DELAY); + dma_channel_configure(v2TxDmaChannel, &txConfig, &uart1_hw->dr, + v2DmaTxBuffer, frameBytes, true); + dma_channel_wait_for_finish_blocking(v2TxDmaChannel); + uart_tx_wait_blocking(uart1); + digitalWrite(rs485Pin, LOW); // Read. + delayMicroseconds(RS485_MODE_SWITCH_DELAY); + v2TxFrames++; + return true; +} + +void EventDispatcher::serviceV2UartDmaRx() { + if (!v2UartDmaActive) { + return; + } + + if (v2RxState == V2_RX_IDLE) { + dma_channel_config rxConfig = dma_channel_get_default_config(v2RxDmaChannel); + channel_config_set_transfer_data_size(&rxConfig, DMA_SIZE_8); + channel_config_set_dreq(&rxConfig, uart_get_dreq(uart1, false)); + channel_config_set_read_increment(&rxConfig, false); + channel_config_set_write_increment(&rxConfig, true); + dma_channel_configure(v2RxDmaChannel, &rxConfig, v2DmaRxBuffer, + &uart1_hw->dr, ppuc::v2::kHeaderBytes, true); + v2RxState = V2_RX_HEADER; + v2RxStateStartUs = micros(); + v2RxDmaRestarts++; + return; + } + + if (v2RxState == V2_RX_HEADER && dma_channel_is_busy(v2RxDmaChannel)) { + if ((micros() - v2RxStateStartUs) > kV2RxTimeoutUs) { + dma_channel_abort(v2RxDmaChannel); + v2RxState = V2_RX_IDLE; + v2RxDmaTimeouts++; + } + return; + } + + if (v2RxState == V2_RX_HEADER) { + if (v2DmaRxBuffer[0] != ppuc::v2::kSyncByte) { + v2RxSyncFail++; + v2RxState = V2_RX_IDLE; + return; + } + ppuc::v2::FrameType frameType = ppuc::v2::ExtractType(v2DmaRxBuffer[1]); + v2RxPayloadBytes = getV2PayloadBytes(frameType); + + dma_channel_config rxConfig = dma_channel_get_default_config(v2RxDmaChannel); + channel_config_set_transfer_data_size(&rxConfig, DMA_SIZE_8); + channel_config_set_dreq(&rxConfig, uart_get_dreq(uart1, false)); + channel_config_set_read_increment(&rxConfig, false); + channel_config_set_write_increment(&rxConfig, true); + dma_channel_configure(v2RxDmaChannel, &rxConfig, + &v2DmaRxBuffer[ppuc::v2::kHeaderBytes], + &uart1_hw->dr, + v2RxPayloadBytes + ppuc::v2::kCrcBytes, true); + v2RxState = V2_RX_BODY; + v2RxStateStartUs = micros(); + return; + } + + if (v2RxState == V2_RX_BODY && dma_channel_is_busy(v2RxDmaChannel)) { + if ((micros() - v2RxStateStartUs) > kV2RxTimeoutUs) { + dma_channel_abort(v2RxDmaChannel); + v2RxState = V2_RX_IDLE; + v2RxDmaTimeouts++; + } + return; + } + + if (v2RxState == V2_RX_BODY) { + processV2Frame(v2DmaRxBuffer, v2RxPayloadBytes); + v2RxState = V2_RX_IDLE; + } +} + +int16_t EventDispatcher::findMappedIndex(const uint16_t* table, uint16_t count, + uint16_t number) { + for (uint16_t i = 0; i < count; ++i) { + if (table[i] == number) { + return (int16_t)i; + } + } + return -1; +} + +void EventDispatcher::updateSwitchBitmap(Event *event) { + // V2 switch reporting is bitmap-based. Legacy switch events still originate + // from the existing switch devices/listeners; this method mirrors those + // events into the dense V2 switch-state RAM bitmap. On token/poll, the board + // sends this bitmap back to the CPU in one V2 switch frame. + int16_t mappedIndex = + findMappedIndex(switchIndexToNumber, runtimeConfig.switchBits, + event->eventId); + if (mappedIndex < 0) { + return; + } + + ppuc::v2::SetBitmapBit(switchStates, (uint16_t)mappedIndex, + event->value != 0); + if (!applyingRemoteSwitchState) { + switchDirty = true; + } +} + +void EventDispatcher::applyOutputStates(const byte *coils, size_t coilBytes, + const byte *lamps, size_t lampBytes) { + // V2 output frames carry full RAM snapshots. To preserve existing + // EventListener behavior, we synthesize legacy events only for changed bits + // (edge detection old snapshot -> new snapshot). This keeps the rest of the + // firmware event-driven without requiring listener rewrites. + for (uint16_t n = 0; n < runtimeConfig.coilBits; ++n) { + bool oldState = ppuc::v2::GetBitmapBit(outputCoils, n); + bool newState = ppuc::v2::GetBitmapBit(coils, n); + if (oldState != newState) { + callListeners( + new Event(EVENT_SOURCE_SOLENOID, coilIndexToNumber[n], + newState ? 1 : 0), + true, false); + } + } + memcpy(outputCoils, coils, coilBytes); + + for (uint16_t n = 0; n < runtimeConfig.lampBits; ++n) { + bool oldState = ppuc::v2::GetBitmapBit(outputLamps, n); + bool newState = ppuc::v2::GetBitmapBit(lamps, n); + if (oldState != newState) { + callListeners( + new Event(EVENT_SOURCE_LIGHT, lampIndexToNumber[n], newState ? 1 : 0), + true, false); + } + } + memcpy(outputLamps, lamps, lampBytes); +} + +void EventDispatcher::applySwitchStates(const byte* switches, + size_t switchBytes) { + // Global switch state is board-to-board on the RS485 bus. CPU/libppuc never + // broadcasts switch states. Every board consumes incoming switch frames and + // emits local switch events for fast-flip/effect listeners. + applyingRemoteSwitchState = true; + for (uint16_t n = 0; n < runtimeConfig.switchBits; ++n) { + bool oldState = ppuc::v2::GetBitmapBit(switchStates, n); + bool newState = ppuc::v2::GetBitmapBit(switches, n); + if (oldState != newState) { + dispatch(new Event(EVENT_SOURCE_SWITCH, switchIndexToNumber[n], + newState ? 1 : 0, true)); + } + } + applyingRemoteSwitchState = false; + memcpy(switchStates, switches, switchBytes); +} + +void EventDispatcher::sendSwitchStateFrame(byte nextBoard) { + // Switch updates are transmitted as a compact V2 frame containing the full + // dense switch bitmap. The CPU selects the responding board via token + // (header.nextBoard in output frame). This board answers once and then + // returns RS485 direction to RX mode. + const size_t switchBytes = ppuc::v2::BitsToBytes(runtimeConfig.switchBits); + const size_t frameBytes = + ppuc::v2::kHeaderBytes + switchBytes + ppuc::v2::kCrcBytes; + + byte* frame = v2DmaTxBuffer; + frame[0] = ppuc::v2::kSyncByte; + frame[1] = ppuc::v2::ComposeTypeAndFlags(ppuc::v2::kFrameSwitchState, + ppuc::v2::kFlagKeyframe); + frame[2] = nextBoard; + frame[3] = txSequence++; + memcpy(&frame[4], switchStates, switchBytes); + + uint16_t crc = + ppuc::v2::Crc16Ccitt(frame, ppuc::v2::kHeaderBytes + switchBytes); + frame[4 + switchBytes] = highByte(crc); + frame[5 + switchBytes] = lowByte(crc); + + if (!v2UartDmaActive || !sendV2FrameUartDma(frame, frameBytes)) { + v2TxFallback++; + digitalWrite(rs485Pin, HIGH); // Write. + delayMicroseconds(RS485_MODE_SWITCH_DELAY); + hwSerial->write(frame, frameBytes); + hwSerial->flush(); + digitalWrite(rs485Pin, LOW); // Read. + delayMicroseconds(RS485_MODE_SWITCH_DELAY); + } + + lastPoll = millis(); +} + +void EventDispatcher::sendSwitchNoChangeFrame(byte nextBoard) { + byte* frame = v2DmaTxBuffer; + frame[0] = ppuc::v2::kSyncByte; + frame[1] = ppuc::v2::ComposeTypeAndFlags(ppuc::v2::kFrameSwitchNoChange, + ppuc::v2::kFlagNone); + frame[2] = nextBoard; + frame[3] = txSequence++; + uint16_t crc = ppuc::v2::Crc16Ccitt(frame, ppuc::v2::kHeaderBytes); + frame[4] = highByte(crc); + frame[5] = lowByte(crc); + + if (!v2UartDmaActive || !sendV2FrameUartDma(frame, ppuc::v2::kResetFrameBytes)) { + v2TxFallback++; + digitalWrite(rs485Pin, HIGH); // Write. + delayMicroseconds(RS485_MODE_SWITCH_DELAY); + hwSerial->write(frame, ppuc::v2::kResetFrameBytes); + hwSerial->flush(); + digitalWrite(rs485Pin, LOW); // Read. + delayMicroseconds(RS485_MODE_SWITCH_DELAY); + } + + v2SwitchNoChangeTx++; + lastPoll = millis(); +} + +bool EventDispatcher::handleV2Frame() { + if (hwSerial->available() < (int)ppuc::v2::kHeaderBytes) { + return false; + } + + if (hwSerial->peek() != ppuc::v2::kSyncByte) { + return false; + } + + if (!readBytes(v2Buffer, ppuc::v2::kHeaderBytes)) { + return false; + } + + ppuc::v2::FrameType frameType = ppuc::v2::ExtractType(v2Buffer[1]); + size_t payloadBytes = getV2PayloadBytes(frameType); + if (frameType != ppuc::v2::kFrameHeartbeat && + frameType != ppuc::v2::kFrameError && + frameType != ppuc::v2::kFrameReset && payloadBytes == 0 && + frameType != ppuc::v2::kFrameOutputState && + frameType != ppuc::v2::kFrameSwitchNoChange) { + return false; + } + + if (!readBytes(&v2Buffer[ppuc::v2::kHeaderBytes], + payloadBytes + ppuc::v2::kCrcBytes)) { + return false; + } + + return processV2Frame(v2Buffer, payloadBytes); +} + +void EventDispatcher::update() { + if (!rs485) { // We're on Core1, the EffectController. Transmit stacked + // events to Core0. + while (!eventQueue.empty()) { + Event *e = eventQueue.front(); + eventQueue.pop(); + callListeners(e, true, false); + } + } else { + while (!eventQueue.empty()) { + Event *e = eventQueue.front(); + eventQueue.pop(); + callListeners(e, true, false); + } + + if (v2UartDmaActive) { + serviceV2UartDmaRx(); + } else { + // Fallback parser is still needed for V2 bootstrap and fault handling: + // - bootstrap: receive initial V2 setup frame before DMA cutover + // - fault path: continue operating if UART DMA transport cannot start + while (hwSerial->available() > 0) { + int firstByte = hwSerial->peek(); + if (firstByte == ppuc::v2::kSyncByte) { + if (!handleV2Frame()) { + break; } + } else { + // Desync/noise, consume one byte and continue. + hwSerial->read(); } } } @@ -267,6 +618,36 @@ void EventDispatcher::update() { callListeners(configEvent, false); } } + + if (debugEnabled && Serial && (millis() - debugLastPrintMs) >= 1000) { + debugLastPrintMs = millis(); + rp2040.idleOtherCore(); + Serial.print("V2DBG board="); + Serial.print(board); + Serial.print(" active="); + Serial.print(v2UartDmaActive ? 1 : 0); + Serial.print(" cutover_ok="); + Serial.print(v2CutoverOk); + Serial.print(" cutover_fail="); + Serial.print(v2CutoverFail); + Serial.print(" rx="); + Serial.print(v2RxFrames); + Serial.print(" rx_crc_fail="); + Serial.print(v2RxCrcFail); + Serial.print(" rx_sync_fail="); + Serial.print(v2RxSyncFail); + Serial.print(" rx_dma_restart="); + Serial.print(v2RxDmaRestarts); + Serial.print(" rx_dma_timeout="); + Serial.print(v2RxDmaTimeouts); + Serial.print(" tx="); + Serial.print(v2TxFrames); + Serial.print(" tx_nochange="); + Serial.print(v2SwitchNoChangeTx); + Serial.print(" tx_fallback="); + Serial.println(v2TxFallback); + rp2040.resumeOtherCore(); + } } uint32_t EventDispatcher::getLastPoll() { diff --git a/src/EventDispatcher/EventDispatcher.h b/src/EventDispatcher/EventDispatcher.h index 5abd4b1..c3a0050 100644 --- a/src/EventDispatcher/EventDispatcher.h +++ b/src/EventDispatcher/EventDispatcher.h @@ -10,9 +10,13 @@ #include +#include + +#include "hardware/dma.h" #include "Event.h" #include "EventListener.h" #include "MultiCoreCrossLink.h" +#include "../PPUCProtocolV2.h" #ifndef MAX_EVENT_LISTENERS #define MAX_EVENT_LISTENERS 32 @@ -35,6 +39,8 @@ class EventDispatcher { MultiCoreCrossLink* getMultiCoreCrossLink(); void setCrossLinkSerial(HardwareSerial& reference); + void setDebug(bool enabled); + void setNextSwitchBoard(byte boardId); void addListener(EventListener* eventListener, char sourceId); @@ -47,23 +53,74 @@ class EventDispatcher { uint32_t getLastPoll(); private: + bool readBytes(byte* buffer, size_t len); + bool handleV2Frame(); + bool startV2UartDmaTransport(); + void stopV2UartDmaTransport(); + void serviceV2UartDmaRx(); + bool sendV2FrameUartDma(const byte* frame, size_t frameBytes); + size_t getV2PayloadBytes(ppuc::v2::FrameType frameType); + bool processV2Frame(const byte* frame, size_t payloadBytes); + void sendSwitchStateFrame(byte nextBoard); + void sendSwitchNoChangeFrame(byte nextBoard); + void applyOutputStates(const byte* coils, size_t coilBytes, const byte* lamps, + size_t lampBytes); + void applySwitchStates(const byte* switches, size_t switchBytes); + void updateSwitchBitmap(Event* event); + int16_t findMappedIndex(const uint16_t* table, uint16_t count, + uint16_t number); + void callListeners(Event* event, bool sendToOtherCore, bool sendToRS485); void callListeners(ConfigEvent* event, bool sendToOtherCore); - Event* stackEvents[EVENT_STACK_SIZE]; - int stackCounter = -1; + std::queue eventQueue; EventListener* eventListeners[MAX_EVENT_LISTENERS]; char eventListenerFilters[MAX_EVENT_LISTENERS]; int numListeners = -1; byte msg[12]; + byte v2Buffer[ppuc::v2::kHeaderBytes + ppuc::v2::kMaxCoilBytes + + ppuc::v2::kMaxLampBytes + ppuc::v2::kCrcBytes]; + byte v2DmaRxBuffer[ppuc::v2::kHeaderBytes + ppuc::v2::kMaxCoilBytes + + ppuc::v2::kMaxLampBytes + ppuc::v2::kCrcBytes]; + byte v2DmaTxBuffer[ppuc::v2::kHeaderBytes + ppuc::v2::kMaxSwitchBytes + + ppuc::v2::kCrcBytes]; + byte outputCoils[ppuc::v2::kMaxCoilBytes] = {0}; + byte outputLamps[ppuc::v2::kMaxLampBytes] = {0}; + byte switchStates[ppuc::v2::kMaxSwitchBytes] = {0}; + uint16_t coilIndexToNumber[ppuc::v2::kMaxCoilBits]; + uint16_t lampIndexToNumber[ppuc::v2::kMaxLampBits]; + uint16_t switchIndexToNumber[ppuc::v2::kMaxSwitchBits]; + byte txSequence = 0; + ppuc::v2::RuntimeConfig runtimeConfig; + bool v2UartDmaActive = false; + enum V2RxState { V2_RX_IDLE = 0, V2_RX_HEADER, V2_RX_BODY }; + V2RxState v2RxState = V2_RX_IDLE; + size_t v2RxPayloadBytes = 0; + uint32_t v2RxStateStartUs = 0; + int v2RxDmaChannel = -1; + int v2TxDmaChannel = -1; + bool debugEnabled = false; + uint32_t debugLastPrintMs = 0; + uint32_t v2CutoverOk = 0; + uint32_t v2CutoverFail = 0; + uint32_t v2RxFrames = 0; + uint32_t v2RxCrcFail = 0; + uint32_t v2RxSyncFail = 0; + uint32_t v2RxDmaRestarts = 0; + uint32_t v2RxDmaTimeouts = 0; + uint32_t v2TxFrames = 0; + uint32_t v2SwitchNoChangeTx = 0; + uint32_t v2TxFallback = 0; + bool switchDirty = false; + bool applyingRemoteSwitchState = false; bool rs485 = false; uint8_t rs485Pin = 0; byte board = 255; - bool error = false; + byte nextSwitchBoard = ppuc::v2::kNoBoard; uint32_t lastPoll; bool running = false; diff --git a/src/IOBoardController.cpp b/src/IOBoardController.cpp index 8e8f0a0..7eeb9de 100644 --- a/src/IOBoardController.cpp +++ b/src/IOBoardController.cpp @@ -2,6 +2,8 @@ #include "EventDispatcher/CrossLinkDebugger.h" +#define SWITCH_DEBOUNCE 10 + IOBoardController::IOBoardController(int cT) { _eventDispatcher = new EventDispatcher(); _eventDispatcher->addListener(this, EVENT_CONFIGURATION); @@ -15,7 +17,13 @@ IOBoardController::IOBoardController(int cT) { // Read bordID. Ideal value at 10bit resolution: (DIP+1)*1023*2/35 -> 58.46 // to 935.3 boardId = 16 - ((int)((analogRead(28) + 29.23) / 58.46)); + m_debug = (boardId & 0b1000) != 0; + if (m_debug) { + boardId -= 8; + } + _eventDispatcher->setBoard(boardId); + _eventDispatcher->setDebug(m_debug); _eventDispatcher->setRS485ModePin(RS485_MODE_PIN); _eventDispatcher->setCrossLinkSerial(Serial1); _multiCoreCrossLink = new MultiCoreCrossLink(); @@ -32,10 +40,10 @@ IOBoardController::IOBoardController(int cT) { void IOBoardController::update() { if (running) { if (activeSwitches) { - switches()->update(); + // nop } if (activeSwitchMatrix) { - switchMatrix()->update(); + //switchMatrix()->update(); } if (activePwmDevices) { pwmDevices()->update(); @@ -75,8 +83,6 @@ void IOBoardController::handleEvent(Event *event) { case EVENT_RESET: // Clear all configurations or reboot the device. _pwmDevices->reset(); - _switches->reset(); - _switchMatrix->reset(); // Issue a delayed reset of the board. // Core 1 should have enough time to turn off it's devices. @@ -89,50 +95,47 @@ void IOBoardController::handleEvent(Event *event) { void IOBoardController::handleEvent(ConfigEvent *event) { if (event->boardId == boardId) { switch (event->topic) { - case CONFIG_TOPIC_SWITCHES: + case CONFIG_TOPIC_SWITCH_MATRIX: switch (event->key) { + case CONFIG_TOPIC_ACTIVE_LOW: + if (event->value) { + _switchMatrix->setActiveLow(); + } + break; + case CONFIG_TOPIC_NUM_ROWS: + rows = (uint8_t)event->value; + _switchMatrix->setNumRows(rows); + _switches->setNumSwitches(MAX_SWITCHES - NUM_COLUMNS - rows); + break; case CONFIG_TOPIC_PORT: port = event->value; break; case CONFIG_TOPIC_NUMBER: - // Ports 15-18 (labeled as 13-16) of IO_16_8_1 are stateful. - _switches->registerSwitch((byte)port, event->value, - (controllerType == CONTROLLER_16_8_1 && - port >= 15 && port <= 18)); - activeSwitches = true; + _switchMatrix->registerSwitch((byte)port, event->value); + activeSwitchMatrix = true; break; } break; - case CONFIG_TOPIC_SWITCH_MATRIX: + case CONFIG_TOPIC_SWITCH_CHAIN: + if (event->key == CONFIG_TOPIC_NEXT_BOARD) { + _eventDispatcher->setNextSwitchBoard((byte)event->value); + } + break; + + case CONFIG_TOPIC_SWITCHES: switch (event->key) { - case CONFIG_TOPIC_ACTIVE_LOW: - if (event->value) { - _switchMatrix->setActiveLow(); - } - break; - case CONFIG_TOPIC_MAX_PULSE_TIME: - _switchMatrix->setPulseTime((byte)event->value); - break; - case CONFIG_TOPIC_TYPE: - type = event->value; - number = 0; - port = 0; + case CONFIG_TOPIC_PORT: + port = event->value; break; case CONFIG_TOPIC_NUMBER: number = event->value; break; - case CONFIG_TOPIC_PORT: - port = event->value; - if (MATRIX_TYPE_COLUMN == type) { - _switchMatrix->registerColumn(port, number); - } else { - _switchMatrix->registerRow(port, number); - } - activeSwitchMatrix = true; + case CONFIG_TOPIC_DEBOUNCE_TIME: + _switches->registerSwitch((byte)port, number, event->value); + activeSwitches = true; break; } - break; case CONFIG_TOPIC_PWM: diff --git a/src/IOBoardController.h b/src/IOBoardController.h index b07f627..9e7240b 100644 --- a/src/IOBoardController.h +++ b/src/IOBoardController.h @@ -1,14 +1,6 @@ /* IOBoardController.h Created by Markus Kalkbrenner. - - GPIO0-7: Input (Switches) or low power output - GPIO8-15: Input (Switches) - GPIO16,17,18: UART TX, UART RX, RS485 Direction - GPIO19-24, 26, 27: Power Out (PWM) - GPIO25: Status-LED - GPIO28: ADC für Adressierung - GPIO29: Reserve (z.B. für einen LED-Strip oder zweite Status-LED) */ #ifndef IOBOARDCONTROLLER_h @@ -40,7 +32,7 @@ class IOBoardController : public EventListener { void update(); - void debug() { m_debug = true; } + bool isDebug() { return m_debug; } private: PwmDevices *_pwmDevices; @@ -58,6 +50,7 @@ class IOBoardController : public EventListener { byte port = 0; byte number = 0; byte power = 0; + byte rows = 0; uint16_t minPulseTime = 0; uint16_t maxPulseTime = 0; byte holdPower = 0; diff --git a/src/IODevices/SwitchMatrix.cpp b/src/IODevices/SwitchMatrix.cpp index ecf466b..7a8eec2 100644 --- a/src/IODevices/SwitchMatrix.cpp +++ b/src/IODevices/SwitchMatrix.cpp @@ -1,113 +1,158 @@ #include "SwitchMatrix.h" -void SwitchMatrix::setActiveLow() { activeLow = true; } +#include "SwitchMatrixPIO/ActiveHigh4Columns.pio.h" +#include "SwitchMatrixPIO/ActiveHigh4Rows.pio.h" +#include "SwitchMatrixPIO/ActiveHigh8Rows.pio.h" +#include "SwitchMatrixPIO/ActiveLow4Columns.pio.h" +#include "SwitchMatrixPIO/ActiveLow4Rows.pio.h" +#include "SwitchMatrixPIO/ActiveLow8Rows.pio.h" -void SwitchMatrix::setPulseTime(byte pT) { pulseTime = pT; } +SwitchMatrix* SwitchMatrix::instance = nullptr; -void SwitchMatrix::registerColumn(byte p, byte n) { - if (n > 0 && n < MAX_COLUMNS) { - columns[n - 1] = p; - pinMode(p, OUTPUT); +void SwitchMatrix::registerSwitch(byte p, byte n) { + if (p < (NUM_COLUMNS * numRows)) { + mapping[p] = n; + active = true; } } -void SwitchMatrix::registerRow(byte p, byte n) { - if (n > 0 && n < MAX_ROWS) { - rows[n - 1] = p; - pinMode(p, INPUT); - } -} +void SwitchMatrix::handleRowChanges(uint32_t raw) { + absolute_time_t now = get_absolute_time(); + uint32_t changed = raw ^ lastStable; // raw to raw comparison -void SwitchMatrix::reset() { - pulseTime = 2; - pauseTime = 2; - activeLow = false; - active = false; - column = 0; - - for (uint8_t col = 0; col < MAX_COLUMNS; col++) { - columns[col] = -1; - for (uint8_t row = 0; row < MAX_ROWS; row++) { - rows[row] = -1; - state[col][row] = 0; - toggled[col][row] = 0; - } - } -} + for (int column = 0; column < NUM_COLUMNS; column++) { + for (int row = 0; row < numRows; row++) { + uint8_t pos = column * numRows + row; + if (mapping[pos] == 0) continue; // Not registered -void SwitchMatrix::update() { - unsigned long ms = millis(); - if (active) { - if (ms > (_ms + (int)(pulseTime / 2))) { - for (int row = 0; row < MAX_ROWS; row++) { - if (rows[row] != -1 && !toggled[column][row]) { - bool new_state = digitalRead(rows[row]); - if (new_state != state[column][row]) { - state[column][row] = new_state; - toggled[column][row] = true; - - word number = (column + 1) * (row + 1); - if (platform != PLATFORM_DATA_EAST) { - number = ((column + 1) * 10) + (row + 1); - } - // Dispatch all switch events as "local fast". - // If a PWM output registered to it, we have "fast flip". Useful for - // flippers, kick backs, jets and sling shots. - _eventDispatcher->dispatch(new Event(EVENT_SOURCE_SWITCH, number, - state[column][row], true)); - } - } - } - } + uint32_t mask = 1u << ((NUM_COLUMNS - 1 - column) * numRows + row); - if (ms > (_ms + pulseTime)) { - digitalWrite(columns[column], activeLow); - active = false; - _ms = ms; - } - } else if (!active && (ms > (_ms + pauseTime))) { - column++; - if (column >= MAX_COLUMNS) { - column = 0; - } + if (changed & mask) { + // Convert RAW to logical pressed/released + // ----------------------------------------- + // activeLow : pressed = raw_bit == 0 + // activeHigh: pressed = raw_bit == 1 + bool rawBit = (raw & mask) != 0; + bool switchState = activeLow ? (!rawBit) // active-low: 0 = pressed + : rawBit; // active-high: 1 = pressed + // Debounce + if (absolute_time_diff_us(debounceTime[pos][switchState], now) >= + MATRIX_SWITCH_DEBOUNCE * 1000) { + debounceTime[pos][switchState] = now; + // Store the *raw* stable state + if (rawBit) + lastStable |= mask; // raw=1 + else + lastStable &= ~mask; // raw=0 - // If column is not in use (-1), the next update will increase the column. - if (columns[column] != -1) { - digitalWrite(columns[column], !activeLow); - active = true; - _ms = ms; + // Dispatch all switch events as "local fast". + // If a PWM output registered to it, we have "fast flip". Useful for + // flippers, kick backs, jets and sling shots. + _eventDispatcher->dispatch(new Event( + EVENT_SOURCE_SWITCH, word(0, mapping[pos]), switchState, true)); + } + } } } } -void SwitchMatrix::handleEvent(Event *event) { +void SwitchMatrix::handleEvent(Event* event) { switch (event->sourceId) { - case EVENT_POLL_EVENTS: - if (boardId == (byte)event->value) { - // This I/O board has been polled for events, so all current switch - // states are transmitted. Reset switch toggles. - for (int col = 0; col < MAX_COLUMNS; col++) { - for (int row = 0; row < MAX_ROWS; row++) { - toggled[col][row] = false; + case EVENT_READ_SWITCHES: + // The CPU requested all current states. Usually this event is sent when + // the game gets started. + if (active) { + // First, send OFF for all switches then ON for the active ones using + // the IRQ handler. + for (int i = 0; i < (NUM_COLUMNS * numRows); i++) { + if (mapping[i] != 0) { + _eventDispatcher->dispatch( + new Event(EVENT_SOURCE_SWITCH, word(0, mapping[i]), 0)); } } - } - break; - case EVENT_READ_SWITCHES: - // The CPU requested all current states. - for (int col = 0; col < MAX_COLUMNS; col++) { - if (columns[col] != -1) { - for (int row = 0; row < MAX_ROWS; row++) { - if (rows[row] != -1) { - word number = (column + 1) * (row + 1); - if (platform != PLATFORM_DATA_EAST) { - number = ((column + 1) * 10) + (row + 1); - } - _eventDispatcher->dispatch( - new Event(EVENT_SOURCE_SWITCH, number, state[column][row])); + if (!running) { + instance = this; + running = true; + + uint columns_offset; + pio_sm_config c_columns; + uint rows_offset; + pio_sm_config c_rows; + + if (activeLow) { + extern const pio_program_t active_low_4_columns_pio_program; + columns_offset = + pio_add_program(pio, &active_low_4_columns_pio_program); + c_columns = active_low_4_columns_pio_program_get_default_config( + columns_offset); + + if (4 == numRows) { + extern const pio_program_t active_low_4_rows_pio_program; + uint rows_offset = + pio_add_program(pio, &active_low_4_rows_pio_program); + pio_sm_config c_rows = + active_low_4_rows_pio_program_get_default_config(rows_offset); + } else { // 8 rows + extern const pio_program_t active_low_8_rows_pio_program; + uint rows_offset = + pio_add_program(pio, &active_low_8_rows_pio_program); + pio_sm_config c_rows = + active_low_8_rows_pio_program_get_default_config(rows_offset); } + } else { // active high + extern const pio_program_t active_high_4_columns_pio_program; + columns_offset = + pio_add_program(pio, &active_high_4_columns_pio_program); + c_columns = active_high_4_columns_pio_program_get_default_config( + columns_offset); + + if (4 == numRows) { + extern const pio_program_t active_high_4_rows_pio_program; + uint rows_offset = + pio_add_program(pio, &active_high_4_rows_pio_program); + pio_sm_config c_rows = + active_high_4_rows_pio_program_get_default_config( + rows_offset); + } else { // 8 rows + extern const pio_program_t active_high_8_rows_pio_program; + uint rows_offset = + pio_add_program(pio, &active_high_8_rows_pio_program); + pio_sm_config c_rows = + active_high_8_rows_pio_program_get_default_config( + rows_offset); + } + } + + // Columns + sm_config_set_in_pins(&c_columns, COLUMNS_BASE_PIN); + // Connect GPIOs to this PIO block + for (uint i = 0; i < NUM_COLUMNS; i++) { + pio_gpio_init(pio, COLUMNS_BASE_PIN + i); + } + // Set the pin direction at the PIO + pio_sm_set_consecutive_pindirs(pio, sm_rows, COLUMNS_BASE_PIN, + NUM_COLUMNS, true); + sm_config_set_out_shift(&c_columns, false, false, 0); + pio_sm_init(pio, sm_columns, columns_offset, &c_columns); + pio_sm_set_enabled(pio, sm_columns, true); + + // Rows + sm_config_set_in_pins(&c_rows, COLUMNS_BASE_PIN - numRows); + // Connect GPIOs to this PIO block + for (uint i = 0; i < (numRows + NUM_COLUMNS); i++) { + pio_gpio_init(pio, COLUMNS_BASE_PIN - numRows + i); } + // Set the pin direction at the PIO, we also read the 4 column pins + pio_sm_set_consecutive_pindirs(pio, sm_rows, + COLUMNS_BASE_PIN - numRows, + numRows + NUM_COLUMNS, false); + sm_config_set_in_shift(&c_rows, false, false, 0); + pio_sm_init(pio, sm_rows, rows_offset, &c_rows); + irq_set_exclusive_handler(PIO0_IRQ_0, onRowChanges); + irq_set_enabled(PIO0_IRQ_0, true); + pio_set_irq0_source_enabled(pio, pis_interrupt0, true); + pio_sm_set_enabled(pio, sm_rows, true); } } break; diff --git a/src/IODevices/SwitchMatrix.h b/src/IODevices/SwitchMatrix.h index 221a8c8..8465def 100644 --- a/src/IODevices/SwitchMatrix.h +++ b/src/IODevices/SwitchMatrix.h @@ -1,6 +1,6 @@ /* SwitchMatrix_h. - Created by Markus Kalkbrenner, 2023. + Created by Markus Kalkbrenner, 2023-2025. */ #ifndef SwitchMatrix_h @@ -8,60 +8,58 @@ #include "../EventDispatcher/Event.h" #include "../EventDispatcher/EventDispatcher.h" -#include "../PPUC.h" +#include "hardware/gpio.h" +#include "hardware/pio.h" -#ifndef MAX_COLUMNS -#define MAX_COLUMNS 10 -#endif - -#ifndef MAX_ROWS +#define COLUMNS_BASE_PIN 15 // GPIO 15-18 for columns, the only pins with required hardware on IO_16_8_1 board +#define NUM_COLUMNS 4 #define MAX_ROWS 8 -#endif +#define MATRIX_SWITCH_DEBOUNCE 2 class SwitchMatrix : public EventListener { public: - SwitchMatrix(byte bId, EventDispatcher *eD) { + SwitchMatrix(byte bId, EventDispatcher* eD) { boardId = bId; - platform = PLATFORM_LIBPINMAME; - - reset(); - - _ms = millis(); _eventDispatcher = eD; _eventDispatcher->addListener(this, EVENT_POLL_EVENTS); _eventDispatcher->addListener(this, EVENT_READ_SWITCHES); } - void registerColumn(byte p, byte n); - void registerRow(byte p, byte n); + void setActiveLow() { activeLow = true; } + void setNumRows(uint8_t n) { numRows = n; } + void registerSwitch(byte p, byte n); - void setActiveLow(); - void setPulseTime(byte pT); + void handleEvent(Event* event); - void update(); - void reset(); + void handleEvent(ConfigEvent* event) {} - void handleEvent(Event *event); + void handleRowChanges(uint32_t raw); - void handleEvent(ConfigEvent *event) {} + PIO pio = pio0; + int sm_columns = 0; + int sm_rows = 1; - private: - byte boardId; - byte platform; - byte pulseTime; - byte pauseTime; - bool activeLow; - bool active; + static SwitchMatrix* instance; - unsigned long _ms; + static void __not_in_flash_func(onRowChanges)() { + // IRQ0 clear + pio0_hw->irq = 1u << 0; - int8_t columns[MAX_COLUMNS]; - int8_t rows[MAX_ROWS]; - bool state[MAX_COLUMNS][MAX_ROWS] = {0}; - bool toggled[MAX_COLUMNS][MAX_ROWS] = {0}; - byte column = 0; + uint32_t raw = pio_sm_get_blocking(instance->pio, instance->sm_rows); + instance->handleRowChanges(raw); + } - EventDispatcher *_eventDispatcher; + private: + byte boardId; + bool activeLow = false; + uint8_t numRows = 4; + bool running = false; + bool active = false; + + byte mapping[NUM_COLUMNS * MAX_ROWS] = {0}; + uint32_t lastStable = 0; + absolute_time_t debounceTime[NUM_COLUMNS * MAX_ROWS][2] = {0}; + EventDispatcher* _eventDispatcher; }; #endif diff --git a/src/IODevices/SwitchMatrix8x16.pio b/src/IODevices/SwitchMatrix8x16.pio new file mode 100644 index 0000000..9bd416c --- /dev/null +++ b/src/IODevices/SwitchMatrix8x16.pio @@ -0,0 +1,114 @@ +.program columns8x16_pio +.wrap_target + set y, 7 ; 8 columns (0-7) + +loop: + mov osr, y ; copy Y to OSR for output + out pins, 8 ; set 8 pins for columns, one is active LOW, others HIGH + nop [16] ; short delay to let other state machines read rows + jmp y-- loop ; decrement Y, loop until Y < 0 +.wrap + + +.program even_rows_high_pio + ; Initialize X to all pins HIGH. + set x, 0 ; X = 0x0 + mov y, ~x ; Y = 0xFFFFFFFF + mov x, y ; X = 0xFFFFFFFF + mov isr, y ; ISR = 0xFFFFFFFF + +loop: + wait 0 GPIO 10 ; wait for even column 8 + in pins, 16 ; read 16 rows to ISR (shifting into lower 16 bits of the 32bit ISR) + wait 0 GPIO 8 ; wait for even column 6 + in pins, 16 ; read 16 rows to ISR (shifting into lower 16 bits of the 32bit ISR) + + mov y, isr ; copy ISR to Y for comparison + jmp x!=y changed ; jump to "changed" if X (old state) != Y (new state) + jmp loop + +changed: + mov osr, y + push block ; push OSR to FIFO, "block" waits until the CPU handled enough data in the FIFO, so we don't overflow it + irq 1 ; trigger interrupt 1 to notify the main CPU that a even columns state has changed + mov x, y ; update X to the new switch states + jmp loop + + +.program odd_rows_high_pio + ; Initialize X to all pins HIGH. + set x, 0 ; X = 0x0 + mov y, ~x ; Y = 0xFFFFFFFF + mov x, y ; X = 0xFFFFFFFF + mov isr, y ; ISR = 0xFFFFFFFF + +loop: + wait 0 GPIO 9 ; wait for odd column 7 + in pins, 16 ; read 16 rows to ISR (shifting into lower 16 bits of the 32bit ISR) + wait 0 GPIO 7 ; wait for odd column 5 + in pins, 16 ; read 16 rows to ISR (shifting into lower 16 bits of the 32bit ISR) + + mov y, isr ; copy ISR to Y for comparison + jmp x!=y changed ; jump to "changed" if X (old state) != Y (new state) + jmp loop + +changed: + mov osr, y + push block ; push OSR to FIFO, "block" waits until the CPU handled enough data in the FIFO, so we don't overflow it + irq 0 ; trigger interrupt 0 to notify the main CPU that a odd columns state has changed + mov x, y ; update X to the new odd columns state + jmp loop + + +; IMPORTANT: +; The "low" PIO programs must run on a separate PIO instance from the "high" PIO programs, +; because of the limitation of interrupts per PIO instance (only 2). + +.program even_rows_low_pio + ; Initialize X to all pins HIGH. + set x, 0 ; X = 0x0 + mov y, ~x ; Y = 0xFFFFFFFF + mov x, y ; X = 0xFFFFFFFF + mov isr, y ; ISR = 0xFFFFFFFF + +loop: + wait 0 GPIO 6 ; wait for even column 4 + in pins, 16 ; read 16 rows to ISR (shifting into lower 16 bits of the 32bit ISR) + wait 0 GPIO 4 ; wait for even column 2 + in pins, 16 ; read 16 rows to ISR (shifting into lower 16 bits of the 32bit ISR) + + mov y, isr ; copy ISR to Y for comparison + jmp x!=y changed ; jump to "changed" if X (old state) != Y (new state) + jmp loop + +changed: + mov osr, y + push block ; push OSR to FIFO, "block" waits until the CPU handled enough data in the FIFO, so we don't overflow it + irq 1 ; trigger interrupt 1 to notify the main CPU that a even columns state has changed + mov x, y ; update X to the new switch states + jmp loop + + +.program odd_rows_low_pio + ; Initialize X to all pins HIGH. + set x, 0 ; X = 0x0 + mov y, ~x ; Y = 0xFFFFFFFF + mov x, y ; X = 0xFFFFFFFF + mov isr, y ; ISR = 0xFFFFFFFF + +loop: + wait 0 GPIO 5 ; wait for odd column 3 + in pins, 16 ; read 16 rows to ISR (shifting into lower 16 bits of the 32bit ISR) + wait 0 GPIO 3 ; wait for odd column 1 + in pins, 16 ; read 16 rows to ISR (shifting into lower 16 bits of the 32bit ISR) + + mov y, isr ; copy ISR to Y for comparison + jmp x!=y changed ; jump to "changed" if X (old state) != Y (new state) + jmp loop + +changed: + mov osr, y + push block ; push OSR to FIFO, "block" waits until the CPU handled enough data in the FIFO, so we don't overflow it + irq 0 ; trigger interrupt 0 to notify the main CPU that a odd columns state has changed + mov x, y ; update X to the new odd columns state + jmp loop diff --git a/src/IODevices/SwitchMatrixPIO/ActiveHigh4Columns.pio b/src/IODevices/SwitchMatrixPIO/ActiveHigh4Columns.pio new file mode 100644 index 0000000..c054f12 --- /dev/null +++ b/src/IODevices/SwitchMatrixPIO/ActiveHigh4Columns.pio @@ -0,0 +1,15 @@ +.program active_high_4_columns_pio +.wrap_target + set x, 1 + mov isr, x ; initialize ISR with 0x00000001 + set x, 0 ; X = 0 + set y, 3 ; 4 columns (0-3) + nop [16] ; short delay to let other state machines perform debouncing + +loop: + mov osr, isr ; copy ISR to OSR for output + out pins, 4 ; set 4 pins for columns, one is HIGH, others LOW + in x, 1 ; shift left ISR by 1 bit + nop [16] ; short delay to let other state machines read rows + jmp y-- loop ; decrement Y, loop until Y < 0 +.wrap diff --git a/src/IODevices/SwitchMatrixPIO/ActiveHigh4Rows.pio b/src/IODevices/SwitchMatrixPIO/ActiveHigh4Rows.pio new file mode 100644 index 0000000..2adad9d --- /dev/null +++ b/src/IODevices/SwitchMatrixPIO/ActiveHigh4Rows.pio @@ -0,0 +1,35 @@ +.program active_high_4_rows_pio + ; Initialize X to all pins LOW + set x, 0 ; X = 0x0 + mov y, x ; Y = 0x0 + mov isr, y ; ISR = 0x0 + mov osr, y ; ISR = 0x0 + +loop: + wait 0 PIN 4 ; wait for column 1 + in pins, 4 ; read 4 rows to ISR (shifting into lower 4 bits of the 32bit ISR) + wait 0 PIN 5 ; wait for column 2 + in pins, 4 ; read 4 rows to ISR (shifting into lower 4 bits of the 32bit ISR) + wait 0 PIN 6 ; wait for odd column 3 + in pins, 4 ; read 4 rows to ISR (shifting into lower 4 bits of the 32bit ISR) + wait 0 PIN 7 ; wait for odd column 4 + in pins, 4 ; read 4 rows to ISR (shifting into lower 4 bits of the 32bit ISR) + + mov y, isr ; copy ISR to X for comparison + jmp x!=y changed + + ; two consecutive identical reads required for debouncing + mov y, osr ; copy OSR to Y for comparison + jmp x!=y new_stable_state + + jmp loop + +changed: + mov x, isr ; update X to the new state + jmp loop + +new_stable_state: + mov osr, isr ; update OSR to the new state, used for debouncing + push block ; push ISR to FIFO, "block" waits until the CPU handled enough data in the FIFO, so we don't overflow it, ISR is empty after that! + irq 0 ; trigger interrupt 0 to notify the main CPU that matrix changed + jmp loop diff --git a/src/IODevices/SwitchMatrixPIO/ActiveHigh8Rows.pio b/src/IODevices/SwitchMatrixPIO/ActiveHigh8Rows.pio new file mode 100644 index 0000000..160ac0e --- /dev/null +++ b/src/IODevices/SwitchMatrixPIO/ActiveHigh8Rows.pio @@ -0,0 +1,35 @@ +.program active_high_8_rows_pio + ; Initialize X to all pins LOW + set x, 0 ; X = 0x0 + mov y, x ; Y = 0x0 + mov isr, y ; ISR = 0x0 + mov osr, y ; ISR = 0x0 + +loop: + wait 0 PIN 8 ; wait for column 1 + in pins, 8 ; read 8 rows to ISR (shifting into lower 8 bits of the 32bit ISR) + wait 0 PIN 9 ; wait for column 2 + in pins, 8 ; read 8 rows to ISR (shifting into lower 8 bits of the 32bit ISR) + wait 0 PIN 10 ; wait for odd column 3 + in pins, 8 ; read 8 rows to ISR (shifting into lower 8 bits of the 32bit ISR) + wait 0 PIN 11 ; wait for odd column 4 + in pins, 8 ; read 8 rows to ISR (shifting into lower 8 bits of the 32bit ISR) + + mov y, isr ; copy ISR to X for comparison + jmp x!=y changed + + ; two consecutive identical reads required for debouncing + mov y, osr ; copy OSR to Y for comparison + jmp x!=y new_stable_state + + jmp loop + +changed: + mov x, isr ; update X to the new state + jmp loop + +new_stable_state: + mov osr, isr ; update OSR to the new state, used for debouncing + push block ; push ISR to FIFO, "block" waits until the CPU handled enough data in the FIFO, so we don't overflow it, ISR is empty after that! + irq 0 ; trigger interrupt 0 to notify the main CPU that matrix changed + jmp loop diff --git a/src/IODevices/SwitchMatrixPIO/ActiveLow4Columns.pio b/src/IODevices/SwitchMatrixPIO/ActiveLow4Columns.pio new file mode 100644 index 0000000..ae3fa59 --- /dev/null +++ b/src/IODevices/SwitchMatrixPIO/ActiveLow4Columns.pio @@ -0,0 +1,16 @@ +.program active_low_4_columns_pio +.wrap_target + set y, 1 + mov isr, ~y ; initialize ISR with 0xFFFFFFFE + set y, 0 ; Y = 0 + mov x, ~y ; X = 0xFFFFFFFF + set y, 3 ; 4 columns (0-3) + nop [16] ; short delay to let other state machines perform debouncing + +loop: + mov osr, isr ; copy ISR to OSR for output + out pins, 4 ; set 4 pins for columns, one is LOW, others HIGH + in x, 1 ; shift left ISR by 1 bit + nop [16] ; short delay to let other state machines read rows + jmp y-- loop ; decrement Y, loop until Y < 0 +.wrap diff --git a/src/IODevices/SwitchMatrixPIO/ActiveLow4Rows.pio b/src/IODevices/SwitchMatrixPIO/ActiveLow4Rows.pio new file mode 100644 index 0000000..cdcc6b8 --- /dev/null +++ b/src/IODevices/SwitchMatrixPIO/ActiveLow4Rows.pio @@ -0,0 +1,37 @@ +.program active_low_4_rows_pio + ; Initialize X to all pins HIGH + set x, 0 ; X = 0x0 + mov y, ~x ; Y = 0xFFFFFFFF + mov x, y ; X = 0xFFFFFFFF + mov osr, y ; OSR = 0xFFFFFFFF + +loop: + set y, 0 ; X = 0x0 + mov isr, ~y ; ISR = 0xFFFFFFFF + wait 0 PIN 4 ; wait for column 1 + in pins, 4 ; read 4 rows to ISR (shifting into lower 4 bits of the 32bit ISR) + wait 0 PIN 5 ; wait for column 2 + in pins, 4 ; read 4 rows to ISR (shifting into lower 4 bits of the 32bit ISR) + wait 0 PIN 6 ; wait for odd column 3 + in pins, 4 ; read 4 rows to ISR (shifting into lower 4 bits of the 32bit ISR) + wait 0 PIN 7 ; wait for odd column 4 + in pins, 4 ; read 4 rows to ISR (shifting into lower 4 bits of the 32bit ISR) + + mov y, isr ; copy ISR to X for comparison + jmp x!=y changed + + ; two consecutive identical reads required for debouncing + mov y, osr ; copy OSR to Y for comparison + jmp x!=y new_stable_state + + jmp loop + +changed: + mov x, isr ; update X to the new state + jmp loop + +new_stable_state: + mov osr, isr ; update OSR to the new state, used for debouncing + push block ; push ISR to FIFO, "block" waits until the CPU handled enough data in the FIFO, so we don't overflow it, ISR is empty after that! + irq 0 ; trigger interrupt 0 to notify the main CPU that matrix changed + jmp loop diff --git a/src/IODevices/SwitchMatrixPIO/ActiveLow8Rows.pio b/src/IODevices/SwitchMatrixPIO/ActiveLow8Rows.pio new file mode 100644 index 0000000..607d270 --- /dev/null +++ b/src/IODevices/SwitchMatrixPIO/ActiveLow8Rows.pio @@ -0,0 +1,37 @@ +.program active_low_8_rows_pio + ; Initialize X to all pins HIGH + set x, 0 ; X = 0x0 + mov y, ~x ; Y = 0xFFFFFFFF + mov x, y ; X = 0xFFFFFFFF + mov osr, y ; OSR = 0xFFFFFFFF + +loop: + set y, 0 ; X = 0x0 + mov isr, ~y ; ISR = 0xFFFFFFFF + wait 0 PIN 8 ; wait for column 1 + in pins, 8 ; read 8 rows to ISR (shifting into lower 8 bits of the 32bit ISR) + wait 0 PIN 9 ; wait for column 2 + in pins, 8 ; read 8 rows to ISR (shifting into lower 8 bits of the 32bit ISR) + wait 0 PIN 10 ; wait for odd column 3 + in pins, 8 ; read 8 rows to ISR (shifting into lower 8 bits of the 32bit ISR) + wait 0 PIN 11 ; wait for odd column 4 + in pins, 8 ; read 8 rows to ISR (shifting into lower 8 bits of the 32bit ISR) + + mov y, isr ; copy ISR to X for comparison + jmp x!=y changed + + ; two consecutive identical reads required for debouncing + mov y, osr ; copy OSR to Y for comparison + jmp x!=y new_stable_state + + jmp loop + +changed: + mov x, isr ; update X to the new state + jmp loop + +new_stable_state: + mov osr, isr ; update OSR to the new state, used for debouncing + push block ; push ISR to FIFO, "block" waits until the CPU handled enough data in the FIFO, so we don't overflow it, ISR is empty after that! + irq 0 ; trigger interrupt 0 to notify the main CPU that matrix changed + jmp loop diff --git a/src/IODevices/Switches.cpp b/src/IODevices/Switches.cpp index fc38531..4e30711 100644 --- a/src/IODevices/Switches.cpp +++ b/src/IODevices/Switches.cpp @@ -1,98 +1,129 @@ #include "Switches.h" -void Switches::registerSwitch(byte p, byte n, bool s) { - if (last < (MAX_SWITCHES - 1)) { - if (s) { - resetStatefulPort(p); - } +#include "SwitchesPIO/ActiveLow16Switches.pio.h" +#include "SwitchesPIO/ActiveLow4Switches.pio.h" +#include "SwitchesPIO/ActiveLow8Switches.pio.h" + +Switches* Switches::instance = nullptr; - pinMode(p, INPUT); +void Switches::registerSwitch(byte p, byte n, uint8_t debounceTimeMs) { + if (last < (numSwitches - 1) && p < numSwitches) { port[++last] = p; number[last] = n; - toggled[last] = false; - stateful[last] = s; - delayMicroseconds(10); - // Note, we have active LOW! - state[last] = !digitalRead(p); + debounceSetting[last] = debounceTimeMs; + active = true; } } -void Switches::resetStatefulPort(byte p) { - // Set mid power output as input. - pinMode(p, OUTPUT); - digitalWrite(p, HIGH); - pinMode(p, INPUT); -} - -void Switches::reset() { - for (uint8_t i = 0; i < MAX_SWITCHES; i++) { - if (stateful[i]) resetStatefulPort(port[i]); - } +void Switches::handleSwitchChanges(uint32_t raw) { + uint32_t now = millis(); + uint32_t changed = raw ^ currentStable; + if (changed > 0) { + uint32_t allSwitchesMask = 0; - for (uint8_t i = 0; i < MAX_SWITCHES; i++) { - port[i] = 0; - number[i] = 0; - state[i] = 0; - toggled[i] = false; - stateful[i] = false; - } - - last = -1; -} - -void Switches::update() { - // Wait for SWITCH_DEBOUNCE milliseconds to debounce the switches. That covers - // the edge case that a switch was hit right before the last polling of - // events. After SWITCH_DEBOUNCE milliseconds every switch is allowed to - // toggle once until the events get polled again. - if (millis() - _ms >= SWITCH_DEBOUNCE) { for (int i = 0; i <= last; i++) { - if (!toggled[i]) { - // Note, we have active LOW! - bool new_state = !digitalRead(port[i]); - if (new_state != state[i]) { - state[i] = new_state; - toggled[i] = true; + uint32_t mask = 1u << (port[i] - SWITCHES_BASE_PIN); + allSwitchesMask |= mask; + + if (changed & mask) { + // Debounce + if ((debounceTime[i] + debounceSetting[i]) < now) { + debounceTime[i] = now; + bool switchState = ((raw & mask) != 0); + if (switchState) + currentStable |= mask; // set bit in lastStable to 1 + else + currentStable &= ~mask; // set bit in lastStable to 0 // Dispatch all switch events as "local fast". // If a PWM output registered to it, we have "fast flip". Useful for // flippers, kick backs, jets and sling shots. - _eventDispatcher->dispatch(new Event( - EVENT_SOURCE_SWITCH, word(0, number[i]), state[i], true)); + _eventDispatcher->dispatch(new Event(EVENT_SOURCE_SWITCH, + word(0, number[i]), + switchState ? 1 : 0, true)); + // digitalWrite(LED_BUILTIN, switchState); } } } + + // Set unregistered switches to raw value to for next comparison + currentStable = + (currentStable & allSwitchesMask) + (raw & ~allSwitchesMask); } + + // Push debounced state to PIO for next detection + pio_sm_put(pio, sm, ~currentStable); } -void Switches::handleEvent(Event *event) { +void Switches::handleEvent(Event* event) { switch (event->sourceId) { - case EVENT_POLL_EVENTS: - if (running && boardId == (byte)event->value) { - // This I/O board has been polled for events, so all current switch - // states are transmitted. Reset switch debounce timer and toggles. - _ms = millis(); + case EVENT_READ_SWITCHES: + // The CPU requested all current states. Usually this event is sent when + // the game gets started. + if (active) { + // First, send OFF for all switches then ON for the active ones using + // the IRQ handler. for (int i = 0; i <= last; i++) { - toggled[i] = false; - if (stateful[i]) resetStatefulPort(port[i]); + if (number[i] != 0) { + uint16_t mask = 1u << (port[i] - SWITCHES_BASE_PIN); + _eventDispatcher->dispatch( + new Event(EVENT_SOURCE_SWITCH, word(0, number[i]), + ((currentStable & mask) > 0) ? 1 : 0)); + } } - } - break; - case EVENT_READ_SWITCHES: - // The CPU requested all current states. - for (int i = 0; i <= last; i++) { - // Send all states of switches that haven't been toggled since last poll - // (and dispatched their event already). - if (!toggled[i]) { - _eventDispatcher->dispatch( - new Event(EVENT_SOURCE_SWITCH, word(0, number[i]), state[i])); - } else { - toggled[i] = false; - if (stateful[i]) resetStatefulPort(port[i]); + if (!running) { + instance = this; + running = true; + + uint offset; + pio_sm_config c; + + switch (numSwitches) { + case 4: + extern const pio_program_t active_low_4_switches_pio_program; + offset = pio_add_program(pio, &active_low_4_switches_pio_program); + c = active_low_4_switches_pio_program_get_default_config(offset); + break; + + case 8: + extern const pio_program_t active_low_8_switches_pio_program; + offset = pio_add_program(pio, &active_low_8_switches_pio_program); + c = active_low_8_switches_pio_program_get_default_config(offset); + break; + + case MAX_SWITCHES: + default: + extern const pio_program_t active_low_16_switches_pio_program; + offset = + pio_add_program(pio, &active_low_16_switches_pio_program); + c = active_low_16_switches_pio_program_get_default_config(offset); + break; + } + + sm_config_set_in_pins(&c, SWITCHES_BASE_PIN); + if (MAX_SWITCHES == numSwitches) { + // Using GPIO 15-18 as switch inputs on IO_16_8_1 board requires + // resetting the sateful input after reading. + // Set begins at GPIO 15 for 4 pins. + sm_config_set_set_pins(&c, 15, 4); + // Side-set begins at GPIO 15. + sm_config_set_sideset_pins(&c, 15); + } + // Connect GPIOs to this PIO block + for (uint i = 0; i < numSwitches; i++) { + pio_gpio_init(pio, SWITCHES_BASE_PIN + i); + } + // Set the pin direction at the PIO + pio_sm_set_consecutive_pindirs(pio, sm, SWITCHES_BASE_PIN, + numSwitches, false); + sm_config_set_in_shift(&c, false, false, 0); + pio_sm_init(pio, sm, offset, &c); + irq_set_exclusive_handler(PIO0_IRQ_1, onSwitchChanges); + irq_set_enabled(PIO0_IRQ_1, true); + pio_set_irq1_source_enabled(pio, pis_interrupt1, true); + pio_sm_set_enabled(pio, sm, true); } } - _ms = millis(); - running = true; break; } } diff --git a/src/IODevices/Switches.h b/src/IODevices/Switches.h index 213ae2c..e79e98d 100644 --- a/src/IODevices/Switches.h +++ b/src/IODevices/Switches.h @@ -12,48 +12,62 @@ #include "../EventDispatcher/Event.h" #include "../EventDispatcher/EventDispatcher.h" +#include "hardware/gpio.h" +#include "hardware/pio.h" -#ifndef MAX_SWITCHES +#define SWITCHES_BASE_PIN 3 #define MAX_SWITCHES 16 -#endif - -#ifndef SWITCH_DEBOUNCE -#define SWITCH_DEBOUNCE 2 -#endif class Switches : public EventListener { public: Switches(byte bId, EventDispatcher* eD) { boardId = bId; - _ms = millis(); _eventDispatcher = eD; _eventDispatcher->addListener(this, EVENT_POLL_EVENTS); _eventDispatcher->addListener(this, EVENT_READ_SWITCHES); } - void registerSwitch(byte p, byte n, bool stateful = false); - - void update(); - void reset(); + void setNumSwitches(uint8_t n) { + numSwitches = n; + validSwitchMask = (1u << numSwitches) - 1; + } + void registerSwitch(byte p, byte n, uint8_t debounceTimeMs); void handleEvent(Event* event); void handleEvent(ConfigEvent* event) {} - private: - void resetStatefulPort(byte p); + void handleSwitchChanges(uint32_t raw); + + PIO pio = pio0; + int sm = 2; // State machine 0 and 1 are used by SwitchMatrix + uint16_t validSwitchMask = (1u << MAX_SWITCHES) - 1; + static Switches* instance; + uint8_t numSwitches = MAX_SWITCHES; + static void __not_in_flash_func(onSwitchChanges)() { + // re-enable IRQ1 for next switch change (clear IRQ 1) + pio0_hw->irq = 1u << 1; + + // Get 32 bit from FIFO + uint32_t raw = pio_sm_get(instance->pio, instance->sm); + instance->handleSwitchChanges((~raw) & instance->validSwitchMask); + } + + private: byte boardId; - unsigned long _ms; + bool running = false; + bool active = false; byte port[MAX_SWITCHES] = {0}; byte number[MAX_SWITCHES] = {0}; - bool state[MAX_SWITCHES] = {0}; - bool toggled[MAX_SWITCHES] = {0}; - bool stateful[MAX_SWITCHES] = {0}; + uint8_t debounceSetting[MAX_SWITCHES] = {0}; + uint32_t debounceTime[MAX_SWITCHES] = {0}; int last = -1; + uint16_t currentStable = 0; + EventDispatcher* _eventDispatcher; }; diff --git a/src/IODevices/SwitchesPIO/ActiveLow16Switches.pio b/src/IODevices/SwitchesPIO/ActiveLow16Switches.pio new file mode 100644 index 0000000..3f4bdbe --- /dev/null +++ b/src/IODevices/SwitchesPIO/ActiveLow16Switches.pio @@ -0,0 +1,46 @@ +.program active_low_16_switches_pio +.side_set 4 opt + set x, 0 ; X = 0x0 + mov osr, ~x + jmp reset_stateful_pins + +loop: + mov isr, null ; ISR is 0x0 + in pins, 16 ; read 16 switches to ISR + mov y, isr ; copy ISR to Y for comparison + jmp x!=y changed ; jump to "changed" if X (old state) != Y (new state) + + ; two consecutive identical reads required for first level of debouncing + mov y, osr ; copy OSR to Y for comparison + jmp x!=y new_stable_state + + jmp loop + +changed: + mov x, y ; update X to the new state + nop ; short delay for debouncing + nop ; short delay for debouncing + nop ; short delay for debouncing + nop ; short delay for debouncing + nop ; short delay for debouncing + nop ; short delay for debouncing + + jmp loop + +new_stable_state: + push ; push ISR to RX FIFO + irq 1 ; trigger interrupt 1 to notify the main CPU that switches changed + pull ; pull debounced state from TX FIFO to OSR + mov x, osr ; update X to the new state from OSR + +reset_stateful_pins: + ; Reset stateful pins (GPIO 15-18) + set pindirs, 0b1111 ; change direction of 4 pins (15-18) to output + nop side 0 ; set LOW + nop side 0b1111 ; set 4 pins to HIGH + nop side 0b1111 ; short delay + nop side 0b1111 ; short delay + nop side 0b1111 ; short delay + nop side 0 ; set LOW + set pindirs, 0 ; change direction all pins back to input + jmp loop diff --git a/src/IODevices/SwitchesPIO/ActiveLow4Switches.pio b/src/IODevices/SwitchesPIO/ActiveLow4Switches.pio new file mode 100644 index 0000000..de3aa17 --- /dev/null +++ b/src/IODevices/SwitchesPIO/ActiveLow4Switches.pio @@ -0,0 +1,28 @@ +.program active_low_4_switches_pio + set x, 0 ; X = 0x0 + mov osr, ~x + +loop: + mov isr, null ; ISR is 0x0 + in pins, 8 ; read 4 switches to ISR + mov y, isr ; copy ISR to Y for comparison + jmp x!=y changed ; jump to "changed" if X (old state) != Y (new state) + + ; two consecutive identical reads required for first level of debouncing + mov y, osr ; copy OSR to Y for comparison + jmp x!=y new_stable_state + + jmp loop + +changed: + mov x, y ; update X to the new state + + jmp loop + +new_stable_state: + push ; push ISR to RX FIFO + irq 1 ; trigger interrupt 1 to notify the main CPU that switches changed + pull ; pull debounced state from TX FIFO to OSR + mov x, osr ; update X to the new state from OSR + + jmp loop diff --git a/src/IODevices/SwitchesPIO/ActiveLow8Switches.pio b/src/IODevices/SwitchesPIO/ActiveLow8Switches.pio new file mode 100644 index 0000000..19c140a --- /dev/null +++ b/src/IODevices/SwitchesPIO/ActiveLow8Switches.pio @@ -0,0 +1,28 @@ +.program active_low_8_switches_pio + set x, 0 ; X = 0x0 + mov osr, ~x + +loop: + mov isr, null ; ISR is 0x0 + in pins, 8 ; read 8 switches to ISR + mov y, isr ; copy ISR to Y for comparison + jmp x!=y changed ; jump to "changed" if X (old state) != Y (new state) + + ; two consecutive identical reads required for first level of debouncing + mov y, osr ; copy OSR to Y for comparison + jmp x!=y new_stable_state + + jmp loop + +changed: + mov x, y ; update X to the new state + + jmp loop + +new_stable_state: + push ; push ISR to RX FIFO + irq 1 ; trigger interrupt 1 to notify the main CPU that switches changed + pull ; pull debounced state from TX FIFO to OSR + mov x, osr ; update X to the new state from OSR + + jmp loop diff --git a/src/PPUCProtocolV2.h b/src/PPUCProtocolV2.h new file mode 100644 index 0000000..ef0e1f3 --- /dev/null +++ b/src/PPUCProtocolV2.h @@ -0,0 +1,218 @@ +#pragma once + +#include +#include + +namespace ppuc { +namespace v2 { + +constexpr uint32_t kBaudRate = 250000; + +constexpr uint8_t kSyncByte = 0xA5; +constexpr uint8_t kNoBoard = 0xFF; +constexpr uint8_t kMaxBoards = 8; + +// Bitmaps are indexed by global device number: bit N => device number N. +// Runtime counts are configured per game and announced with SetupFrame. +constexpr uint16_t kDefaultCoilBits = 24; +constexpr uint16_t kDefaultLampBits = 64; +constexpr uint16_t kDefaultSwitchBits = 64; +constexpr uint16_t kMaxCoilBits = 256; +constexpr uint16_t kMaxLampBits = 256; +constexpr uint16_t kMaxSwitchBits = 256; + +constexpr size_t BitsToBytes(uint16_t bits) { return (bits + 7u) / 8u; } + +constexpr size_t kDefaultCoilBytes = BitsToBytes(kDefaultCoilBits); +constexpr size_t kDefaultLampBytes = BitsToBytes(kDefaultLampBits); +constexpr size_t kDefaultSwitchBytes = BitsToBytes(kDefaultSwitchBits); +constexpr size_t kMaxCoilBytes = BitsToBytes(kMaxCoilBits); +constexpr size_t kMaxLampBytes = BitsToBytes(kMaxLampBits); +constexpr size_t kMaxSwitchBytes = BitsToBytes(kMaxSwitchBits); + +constexpr size_t kHeaderBytes = 4; +constexpr size_t kCrcBytes = 2; + +enum FrameType : uint8_t { + kFrameOutputState = 0x01, + kFrameSwitchState = 0x02, + kFrameHeartbeat = 0x03, + kFrameError = 0x04, + kFrameSetup = 0x05, + kFrameMapping = 0x06, + kFrameReset = 0x07, + kFrameConfig = 0x08, + kFrameSwitchNoChange = 0x09, +}; + +enum MappingDomain : uint8_t { + kDomainCoil = 0x01, + kDomainLamp = 0x02, + kDomainSwitch = 0x03, +}; + +enum FrameFlag : uint8_t { + kFlagNone = 0x00, + kFlagKeyframe = 0x10, + kFlagDelta = 0x20, + kFlagError = 0x80, +}; + +struct FrameHeader { + uint8_t sync; + uint8_t typeAndFlags; + uint8_t nextBoard; + uint8_t sequence; +}; + +struct SetupPayload { + uint16_t coilBits; + uint16_t lampBits; + uint16_t switchBits; +}; + +struct MappingPayload { + uint8_t domain; + uint8_t reserved; + uint16_t index; + uint16_t number; +}; + +struct ConfigPayload { + uint8_t boardId; + uint8_t topic; + uint8_t index; + uint8_t key; + uint32_t value; +}; + +struct OutputPayload { + // Only first BitsToBytes(coilBits/lampBits) bytes are used at runtime. + uint8_t coils[kMaxCoilBytes]; + uint8_t lamps[kMaxLampBytes]; +}; + +struct SwitchPayload { + // Only first BitsToBytes(switchBits) bytes are used at runtime. + uint8_t switches[kMaxSwitchBytes]; +}; + +struct SetupFrame { + FrameHeader header; + SetupPayload payload; + uint16_t crc; +}; + +struct MappingFrame { + FrameHeader header; + MappingPayload payload; + uint16_t crc; +}; + +struct ConfigFrame { + FrameHeader header; + ConfigPayload payload; + uint16_t crc; +}; + +struct OutputStateFrame { + FrameHeader header; + OutputPayload payload; + uint16_t crc; +}; + +struct SwitchStateFrame { + FrameHeader header; + SwitchPayload payload; + uint16_t crc; +}; + +constexpr size_t kSetupPayloadBytes = sizeof(SetupPayload); +constexpr size_t kMappingPayloadBytes = sizeof(MappingPayload); +constexpr size_t kConfigPayloadBytes = sizeof(ConfigPayload); +constexpr size_t kOutputPayloadBytes = sizeof(OutputPayload); +constexpr size_t kSwitchPayloadBytes = sizeof(SwitchPayload); +constexpr size_t kResetFrameBytes = kHeaderBytes + kCrcBytes; +constexpr size_t kSetupFrameBytes = sizeof(SetupFrame); +constexpr size_t kMappingFrameBytes = sizeof(MappingFrame); +constexpr size_t kConfigFrameBytes = sizeof(ConfigFrame); +constexpr size_t kOutputFrameBytes = sizeof(OutputStateFrame); +constexpr size_t kSwitchFrameBytes = sizeof(SwitchStateFrame); + +struct RuntimeConfig { + uint16_t coilBits = kDefaultCoilBits; + uint16_t lampBits = kDefaultLampBits; + uint16_t switchBits = kDefaultSwitchBits; +}; + +inline bool IsValidRuntimeConfig(const RuntimeConfig& cfg) { + return cfg.coilBits > 0 && cfg.coilBits <= kMaxCoilBits && cfg.lampBits > 0 && + cfg.lampBits <= kMaxLampBits && cfg.switchBits > 0 && + cfg.switchBits <= kMaxSwitchBits; +} + +inline size_t OutputPayloadBytes(const RuntimeConfig& cfg) { + return BitsToBytes(cfg.coilBits) + BitsToBytes(cfg.lampBits); +} + +inline size_t SwitchPayloadBytes(const RuntimeConfig& cfg) { + return BitsToBytes(cfg.switchBits); +} + +inline size_t OutputFrameBytes(const RuntimeConfig& cfg) { + return kHeaderBytes + OutputPayloadBytes(cfg) + kCrcBytes; +} + +inline size_t SwitchFrameBytes(const RuntimeConfig& cfg) { + return kHeaderBytes + SwitchPayloadBytes(cfg) + kCrcBytes; +} + +inline uint16_t Crc16Ccitt(const uint8_t* data, size_t len) { + uint16_t crc = 0xFFFF; + for (size_t i = 0; i < len; ++i) { + crc ^= static_cast(data[i]) << 8; + for (uint8_t bit = 0; bit < 8; ++bit) { + if (crc & 0x8000) { + crc = static_cast((crc << 1) ^ 0x1021); + } else { + crc <<= 1; + } + } + } + return crc; +} + +inline uint8_t ComposeTypeAndFlags(FrameType type, uint8_t flags) { + return static_cast(static_cast(type) | flags); +} + +inline FrameType ExtractType(uint8_t typeAndFlags) { + return static_cast(typeAndFlags & 0x0F); +} + +inline uint8_t ExtractFlags(uint8_t typeAndFlags) { + return static_cast(typeAndFlags & 0xF0); +} + +inline bool IsValidBoard(uint8_t board) { + return board == kNoBoard || board < kMaxBoards; +} + +inline void SetBitmapBit(uint8_t* bitmap, uint16_t number, bool on) { + const uint16_t byteIndex = number / 8u; + const uint8_t bitMask = static_cast(1u << (number % 8u)); + if (on) { + bitmap[byteIndex] |= bitMask; + } else { + bitmap[byteIndex] &= static_cast(~bitMask); + } +} + +inline bool GetBitmapBit(const uint8_t* bitmap, uint16_t number) { + const uint16_t byteIndex = number / 8u; + const uint8_t bitMask = static_cast(1u << (number % 8u)); + return (bitmap[byteIndex] & bitMask) != 0; +} + +} // namespace v2 +} // namespace ppuc diff --git a/src/PPUCTimings.h b/src/PPUCTimings.h index 91b5929..821bc01 100644 --- a/src/PPUCTimings.h +++ b/src/PPUCTimings.h @@ -6,9 +6,9 @@ #ifndef PPUC_TIMINGS_h #define PPUC_TIMINGS_h -#define WAIT_FOR_EFFECT_CONTROLLER_RESET 3000 // 3 seconds -#define WAIT_FOR_SERIAL_DEBUGGER_TIMEOUT 1000 // 1 second -#define WAIT_FOR_IO_BOARD_BOOT 1000 // 1 second +#define WAIT_FOR_EFFECT_CONTROLLER_RESET 3000 // 3 seconds +#define WAIT_FOR_SERIAL_DEBUGGER_TIMEOUT 1000 // 1 second +#define WAIT_FOR_IO_BOARD_BOOT 1000 // 1 second #define WAIT_FOR_IO_BOARD_RESET \ (WAIT_FOR_SERIAL_DEBUGGER_TIMEOUT + WAIT_FOR_EFFECT_CONTROLLER_RESET + \ diff --git a/src/main.cpp b/src/main.cpp index efaf71e..ae525ed 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,7 @@ #include "EventDispatcher/CrossLinkDebugger.h" #include "IOBoardController.h" #include "PPUC.h" +#include "PPUCProtocolV2.h" #include "RPi_Pico_TimerInterrupt.h" IOBoardController ioBoardController(CONTROLLER_16_8_1); @@ -32,7 +33,7 @@ bool watchdog(struct repeating_timer *t) { } bool usb_debugging = false; -bool core_0_initilized = false; +bool core_0_initialized = false; // Each controller will be bound to its own core and has it's own // EventDispatcher. Only the EventDispatcher of IOBoardController @@ -44,51 +45,56 @@ void setup() { // Overclock according to Raspberry Pi Pico SDK recommendations. set_sys_clock_khz(SYS_CLK_KHZ, true); - uint32_t timeout = millis() + WAIT_FOR_SERIAL_DEBUGGER_TIMEOUT; - - Serial.begin(115200); - // Wait for a serial connection of a debugger via USB. Serial us USB CDC - // The Pico implements USB itself so special care must be taken. Use - // while(!Serial){} in the setup() code before printing anything so that it - // waits for the USB connection to be established. - // https://community.platformio.org/t/serial-monitor-not-working/1512/25 - while (!Serial && millis() < timeout) { - } - - if (Serial) { - usb_debugging = true; - ioBoardController.debug(); - delay(10); - ioBoardController.eventDispatcher()->addListener(new CrossLinkDebugger()); - } else { - Serial.end(); - } - - core_0_initilized = true; - rp2040.restartCore1(); - // RS485 connection. Serial1.end(); // Deactivete UART to empty TX FIFO after reboot delay(5); pinMode(RS485_MODE_PIN, OUTPUT); digitalWrite(RS485_MODE_PIN, LOW); // Read mode delay(5); - Serial1.begin(115200); + Serial1.begin(ppuc::v2::kBaudRate); // Empty RX FIFO after reboot while (Serial1.available()) { Serial1.read(); } - // The watchdog interferes with the USB debuging. - if (!usb_debugging) { + usb_debugging = ioBoardController.isDebug(); + + if (usb_debugging) { + pinMode(LED_BUILTIN, OUTPUT); + Serial.begin(115200); + delay(100); + // Wait for a serial connection of a debugger via USB CDC. + // The Pico implements USB itself so special care must be taken. Use + // while(!Serial){} in the setup() code before printing anything so that + // it waits for the USB connection to be established. + // https://community.platformio.org/t/serial-monitor-not-working/1512/25 + while (!Serial) { + digitalWrite(LED_BUILTIN, HIGH); + delay(100); + digitalWrite(LED_BUILTIN, LOW); + delay(100); + digitalWrite(LED_BUILTIN, HIGH); + delay(100); + digitalWrite(LED_BUILTIN, LOW); + delay(1000); + } + + Serial.println("USB Serial debugging active."); + // ioBoardController.eventDispatcher()->addListener(new CrossLinkDebugger()); + } else { + // The watchdog interferes with the USB debuging, so only start it + // if USB debugging is not active. if (!ITimer.attachInterruptInterval(1000000, watchdog)) { // @todo } } + + core_0_initialized = true; + rp2040.restartCore1(); } void setup1() { - while (!core_0_initilized) { + while (!core_0_initialized) { } if (usb_debugging) {