From 60a69992ff9b2f009c9557f95d9d88ac9b9339ca Mon Sep 17 00:00:00 2001 From: Mark Benson Date: Fri, 5 Sep 2025 16:55:54 +0100 Subject: [PATCH 01/12] commit just to save the code at this stage --- software/firmware/tester_runtime/src/main.c | 559 ++++++++++++++------ 1 file changed, 391 insertions(+), 168 deletions(-) diff --git a/software/firmware/tester_runtime/src/main.c b/software/firmware/tester_runtime/src/main.c index c7348ac..90be9d8 100644 --- a/software/firmware/tester_runtime/src/main.c +++ b/software/firmware/tester_runtime/src/main.c @@ -1,46 +1,44 @@ #include "ch32fun.h" -#include #include -// Status Pins +// -------------------- Pin definitions -------------------- + +// Status LEDs (active LOW) #define PIN_PWR PC5 #define PIN_INIT PC6 #define PIN_RDY PC7 #define PIN_RUN PD0 #define PIN_IDLE PD6 -// Test Case Columns (TC1–TC5) +// Test Case Columns (TC1–TC5), active HIGH #define PIN_COL_A PC0 #define PIN_COL_B PC1 #define PIN_COL_C PC2 #define PIN_COL_D PC3 #define PIN_COL_E PC4 -// Test Case Rows +// Test Case Rows (active LOW) #define PIN_ROW_R PA1 // Red (fail) #define PIN_ROW_G PA2 // Green (pass) -// Input buttons +// Input buttons (assumed active LOW with pull-ups) #define PIN_INPUT_A PD2 #define PIN_INPUT_B PD3 #define PIN_INPUT_C PD4 #define PIN_INPUT_D PD5 -// Reset button is connected to NRST pin PD7 which should reset the MCU when pressed. -// This is a a hardware feature and does not require configuration. +// -------------------- Timing -------------------- -const uint8_t statusLEDs[] = { PIN_PWR, PIN_INIT, PIN_RDY, PIN_RUN, PIN_IDLE }; -const uint8_t testCols[] = { PIN_COL_A, PIN_COL_B, PIN_COL_C, PIN_COL_D, PIN_COL_E }; -const uint8_t testRows[] = { PIN_ROW_R, PIN_ROW_G }; -const uint8_t rowRed = PIN_ROW_R; -const uint8_t rowGreen = PIN_ROW_G; +#define TICKS_PER_SECOND 1000 // 1 ms tick -const uint8_t inputPins[] = { PIN_INPUT_A, PIN_INPUT_B, PIN_INPUT_C, PIN_INPUT_D }; +// -------------------- Tables -------------------- -#define FLASH_INTERVAL_MS 10 // Flash every 10ms +static const uint16_t statusLEDs[] = { PIN_PWR, PIN_INIT, PIN_RDY, PIN_RUN, PIN_IDLE }; +static const uint16_t testCols[] = { PIN_COL_A, PIN_COL_B, PIN_COL_C, PIN_COL_D, PIN_COL_E }; +static const uint16_t testRows[] = { PIN_ROW_R, PIN_ROW_G }; +static const uint16_t inputPins[] = { PIN_INPUT_A, PIN_INPUT_B, PIN_INPUT_C, PIN_INPUT_D }; -volatile bool runMode = true; // Current mode: true = RUN, false = IDLE -volatile bool ledState = false; // Current LED state (on/off) +// -------------------- State and flags -------------------- typedef enum { TC_NO_RESULT, @@ -50,238 +48,463 @@ typedef enum { TC_RETRY } TestCaseState; -volatile TestCaseState testCaseStates[5]; // TC_PASS, TC_FAIL, etc. +volatile bool runMode = false; // false = IDLE, true = RUN +volatile bool flashState = false; // toggles every 250 ms +volatile uint32_t msTicks = 0; // monotonic ms counter +volatile uint8_t currentCol = 0; // scan index -volatile uint16_t tickCount = 0; -volatile bool flashState = false; +// Double-buffered states (publish by pointer swap) +static TestCaseState bufA[5], bufB[5]; +static volatile const TestCaseState* activeStates = bufA; +static uint8_t activeIdx = 0; -void scanTestCaseLEDs(void); +// Optional: trap registers for debug (RISC-V) +volatile uint32_t last_mcause = 0, last_mepc = 0; -void initTestCaseStates() { - for (int i = 0; i < 5; i++) { - testCaseStates[i] = TC_NO_RESULT; + +// --- Direct GPIO helpers --- +#define BIT(n) (1U << (n)) +static inline void gpio_set(GPIO_TypeDef* p, uint16_t m) { p->BSHR = m; } +static inline void gpio_clear(GPIO_TypeDef* p, uint16_t m) { p->BCR = m; } + +// --- Explicit port/mask map for columns and rows --- +static const struct { GPIO_TypeDef* port; uint16_t mask; } COL[5] = { + {GPIOC, BIT(0)}, {GPIOC, BIT(1)}, {GPIOC, BIT(2)}, {GPIOC, BIT(3)}, {GPIOC, BIT(4)} +}; +static const struct { GPIO_TypeDef* port; uint16_t mask; } ROW_RED = { GPIOA, BIT(1) }; +static const struct { GPIO_TypeDef* port; uint16_t mask; } ROW_GREEN = { GPIOA, BIT(2) }; + + +// -------------------- Hard fault handler -------------------- + +void HardFault_Handler(void) +{ + __disable_irq(); + asm volatile ("csrr %0, mcause" : "=r"(last_mcause)); + asm volatile ("csrr %0, mepc" : "=r"(last_mepc)); + for (;;) { /* spin */ } +} + +// void HardFault_Handler(void) +// { +// asm volatile ("csrr %0, mcause" : "=r"(last_mcause)); +// asm volatile ("csrr %0, mepc" : "=r"(last_mepc)); + +// // Rapid flash INIT so it's visible +// while (1) { +// funDigitalWrite(PIN_INIT, FUN_LOW); +// for (volatile int i = 0; i < 60000; i++) {}; +// funDigitalWrite(PIN_INIT, FUN_HIGH); +// for (volatile int i = 0; i < 60000; i++) {}; +// } +// } + +// -------------------- Helpers -------------------- + +static inline void initTestCaseStates(void) { + for (int i = 0; i < 5; i++) { bufA[i] = bufB[i] = TC_NO_RESULT; } + activeStates = bufA; + activeIdx = 0; +} + +static inline void setTestCaseResult(const TestCaseState states[5]) { + // Copy into the inactive buffer with IRQs enabled + TestCaseState* dst = (activeIdx == 0) ? bufB : bufA; + for (int i = 0; i < 5; i++) dst[i] = states[i]; + + // Publish with a tiny critical section (pointer swap) + compiler barriers + __disable_irq(); + __asm volatile ("" ::: "memory"); + activeStates = (volatile const TestCaseState*)dst; + activeIdx ^= 1; + __asm volatile ("" ::: "memory"); + __enable_irq(); +} + + +// static inline void scanStep(void) { +// static bool rdy = false; +// rdy = !rdy; +// funDigitalWrite(PIN_RDY, rdy ? FUN_LOW : FUN_HIGH); // active LOW +// } + +// static inline void scanStep(void) { +// gpio_set(COL[0].port, COL[0].mask); // active HIGH +// } + +static inline void scanStep(void) { + gpio_set(COL[1].port, COL[1].mask); + gpio_clear(COL[1].port, COL[1].mask); +} + + +// Phased probe: one write per tick to find the wedging operation +// static inline void scanStep(void) { +// static uint8_t phase = 0; // advances 0..7 repeatedly +// static int8_t prev = -1; // previous column index +// static uint8_t col = 0; // current column index (0..4) + +// // Show ISR progress by ticking RDY each tick (toggle) +// static bool rdy = false; +// rdy = !rdy; +// funDigitalWrite(PIN_RDY, rdy ? FUN_LOW : FUN_HIGH); // active LOW + +// switch (phase) { +// case 0: +// // 0) rows OFF (active LOW -> set HIGH) +// gpio_set(ROW_RED.port, ROW_RED.mask); +// break; + +// case 1: +// // 1) rows OFF (green) +// gpio_set(ROW_GREEN.port, ROW_GREEN.mask); +// break; + +// case 2: +// // 2) turn OFF previous column (if any) +// if (prev >= 0) gpio_clear(COL[prev].port, COL[prev].mask); +// break; + +// case 3: +// // 3) drive rows for current column: PASS => green ON (LOW) +// gpio_clear(ROW_GREEN.port, ROW_GREEN.mask); +// break; + +// case 4: +// // 4) turn ON current column (active HIGH) +// gpio_set(COL[col].port, COL[col].mask); +// break; + +// case 5: +// // 5) do nothing (gap) +// break; + +// case 6: +// // 6) advance column index +// prev = col; +// col = (uint8_t)((col + 1) % 5); +// break; + +// case 7: +// // 7) do nothing (gap) +// break; +// } + +// // Advance to next phase each tick +// phase = (uint8_t)((phase + 1) & 7); +// } + + +// static inline void scanStep(void) { +// static int8_t prevCol = -1; + +// // Turn off previous column (active HIGH -> write LOW) +// if (prevCol >= 0) { +// // funDigitalWrite(testCols[prevCol], FUN_LOW); +// } + +// // Default rows OFF (active LOW -> write HIGH) +// // funDigitalWrite(PIN_ROW_R, FUN_HIGH); +// // funDigitalWrite(PIN_ROW_G, FUN_HIGH); + +// // Drive rows for this column based on state +// const volatile TestCaseState* states = activeStates; +// TestCaseState state = states[currentCol]; + +// switch (state) { +// case TC_NO_RESULT: +// // both OFF +// break; +// case TC_PASS: +// // funDigitalWrite(PIN_ROW_G, FUN_LOW); +// break; +// case TC_FAIL: +// // funDigitalWrite(PIN_ROW_R, FUN_LOW); +// break; +// case TC_IN_PROGRESS: +// if (flashState) { +// // funDigitalWrite(PIN_ROW_R, FUN_LOW); +// } +// else { +// // funDigitalWrite(PIN_ROW_G, FUN_LOW); +// } +// break; +// case TC_RETRY: +// if (currentCol == 4) { // only TC5 flashes red +// // if (flashState) funDigitalWrite(PIN_ROW_R, FUN_LOW); +// } +// break; +// } + +// // Turn on this column last (active HIGH) +// // funDigitalWrite(testCols[currentCol], FUN_HIGH); + +// // Advance +// prevCol = currentCol; +// currentCol = (currentCol + 1) % 5; +// } + +static inline void waitTicks(uint32_t ticks) { + uint32_t start = msTicks; + while ((uint32_t)(msTicks - start) < ticks) { + __WFI(); } } -// Forward declare with correct attribute (WCH RISC-V ISR) -void TIM1_UP_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast"))); +static inline void serviceStatusLeds(void) { + // Update RUN/IDLE (active-low) outside ISR + static bool lastFlash = 0, lastMode = 0; + if (flashState != lastFlash || runMode != lastMode) { + uint16_t activePin = runMode ? PIN_RUN : PIN_IDLE; + uint16_t inactivePin = runMode ? PIN_IDLE : PIN_RUN; + funDigitalWrite(activePin, flashState ? FUN_LOW : FUN_HIGH); + funDigitalWrite(inactivePin, FUN_HIGH); + lastFlash = flashState; + lastMode = runMode; + } +} +// -------------------- Timer ISR -------------------- + +void TIM1_UP_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast"))); void TIM1_UP_IRQHandler(void) { if (TIM1->INTFR & TIM_UIF) { + // Clear update flag only TIM1->INTFR &= ~TIM_UIF; - tickCount++; - // Fast task: scan test case LEDs every tick - scanTestCaseLEDs(); + msTicks++; + scanStep(); - // Slow task: toggle status LED every 500ms (50 ticks) - if (tickCount >= 50) { - tickCount = 0; + // Flip flashState every 250 ms for TC blinking + static uint32_t nextFlashAt = 250; + if (msTicks >= nextFlashAt) { + nextFlashAt += 250; flashState = !flashState; - - uint8_t activePin = runMode ? PIN_RUN : PIN_IDLE; - uint8_t inactivePin = runMode ? PIN_IDLE : PIN_RUN; - - funDigitalWrite(activePin, flashState ? FUN_LOW : FUN_HIGH); - funDigitalWrite(inactivePin, FUN_HIGH); } } } -// Non-blocking delay using tickCount -void waitTicks(uint16_t ticks) { - uint16_t start = tickCount; - while ((tickCount - start) < ticks) { - // Let ISR run - printf("."); - } -} - +// -------------------- Hardware setup -------------------- -// Initialize status LED pins and TIM1 for periodic toggling -void setupStatusFlasher(void) +static inline void setupStatusFlasher(void) { - // Enable and reset TIM1 (on APB2) + // Enable and reset TIM1 (APB2) RCC->APB2PCENR |= RCC_APB2Periph_TIM1; RCC->APB2PRSTR |= RCC_APB2Periph_TIM1; RCC->APB2PRSTR &= ~RCC_APB2Periph_TIM1; - // 48MHz core -> 1kHz timer tick - TIM1->PSC = 48000 - 1; // prescaler to 1 kHz - TIM1->ATRLR = FLASH_INTERVAL_MS - 1; // ARR = 499 -> 500ms - TIM1->SWEVGR |= TIM_UG; // load PSC/ARR - TIM1->INTFR &= ~TIM_UIF; // clear any pending update flag - TIM1->DMAINTENR |= TIM_UIE; // enable update interrupt + // 48 MHz -> 1 MHz timer, ARR=1000-1 => 1 ms + TIM1->PSC = 48 - 1; + TIM1->ATRLR = 1000 - 1; + TIM1->SWEVGR |= TIM_UG; + TIM1->INTFR &= ~TIM_UIF; + TIM1->DMAINTENR |= TIM_UIE; - NVIC_EnableIRQ(TIM1_UP_IRQn); // enable TIM1 update IRQ in NVIC - TIM1->CTLR1 |= TIM_CEN; // start TIM1 + NVIC_EnableIRQ(TIM1_UP_IRQn); + TIM1->CTLR1 |= TIM_CEN; } -// Switch which status LED is flashing -void runStatus(bool isRun) +static inline void runStatus(bool isRun) { - runMode = isRun; - ledState = false; - - // Ensure both OFF immediately; ISR will take over + runMode = isRun; + // ensure both OFF, main loop will handle blinking funDigitalWrite(PIN_RUN, FUN_HIGH); funDigitalWrite(PIN_IDLE, FUN_HIGH); } -void setupPins() { +static inline void setupPins(void) { funGpioInitAll(); - // Status LEDs (LEDs are active LOW) + // Explicit clocks for ports we touch directly + RCC->APB2PCENR |= RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD; + + // Status LEDs (active LOW) for (int i = 0; i < 5; i++) { funPinMode(statusLEDs[i], GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); - funDigitalWrite(statusLEDs[i], FUN_HIGH); // Ensure all status LEDs start OFF + funDigitalWrite(statusLEDs[i], FUN_HIGH); // OFF } - // Test Case LEDs - - // Columns (active HIGH, but row must be LOW to light LED) + // Columns configured via ch32fun (active HIGH, start OFF) for (int i = 0; i < 5; i++) { funPinMode(testCols[i], GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); - funDigitalWrite(testCols[i], FUN_LOW); // Start all columns LOW + funDigitalWrite(testCols[i], FUN_LOW); } - // Rows (active LOW) - for (int i = 0; i < 2; i++) { - funPinMode(testRows[i], GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); - funDigitalWrite(testRows[i], FUN_HIGH); // Start all columns LOW - } -} - + // Rows (active LOW → OFF = HIGH) + funPinMode(PIN_ROW_R, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); + funPinMode(PIN_ROW_G, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); + funDigitalWrite(PIN_ROW_R, FUN_HIGH); + funDigitalWrite(PIN_ROW_G, FUN_HIGH); -int buttonPressed() { - // Returns the index of the button pressed (0-3) or -1 if none pressed + // Inputs with pull-ups for (int i = 0; i < 4; i++) { - if (funDigitalRead(inputPins[i]) == FUN_LOW) { - return i; - } - } - return -1; + funPinMode(inputPins[i], GPIO_CNF_IN_PUPD); + funDigitalWrite(inputPins[i], FUN_HIGH); + } } -void startupSequence() { + +// static inline void setupPins(void) { +// funGpioInitAll(); +// RCC->APB2PCENR |= RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD; + + +// // Status LEDs (active LOW) +// for (int i = 0; i < 5; i++) { +// funPinMode(statusLEDs[i], GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); +// funDigitalWrite(statusLEDs[i], FUN_HIGH); // OFF +// } + +// // Test case columns (active HIGH) +// for (int i = 0; i < 5; i++) { +// funPinMode(testCols[i], GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); +// funDigitalWrite(testCols[i], FUN_LOW); // start OFF +// } + +// // Test case rows (active LOW -> set HIGH to turn OFF) +// for (int i = 0; i < 2; i++) { +// funPinMode(testRows[i], GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); +// funDigitalWrite(testRows[i], FUN_HIGH); +// } + +// // Inputs with pull-ups +// for (int i = 0; i < 4; i++) { +// funPinMode(inputPins[i], GPIO_CNF_IN_PUPD); +// funDigitalWrite(inputPins[i], FUN_HIGH); +// } +// } + +static inline void startupSequence(void) { funDigitalWrite(PIN_PWR, FUN_LOW); // PWR stays on - // blink INIT for 3 times + + // Blink INIT 3 times for (int i = 0; i < 3; i++) { funDigitalWrite(PIN_INIT, FUN_LOW); Delay_Ms(200); funDigitalWrite(PIN_INIT, FUN_HIGH); Delay_Ms(200); } - // Turn on RDY - funDigitalWrite(PIN_RDY, FUN_LOW); - // Turn on IDLE (flashing) - setupStatusFlasher(); // Start timer - runStatus(false); // Set IDLE flashing -} -// Set the test case results via wrapper function -void setTestCaseResult(TestCaseState states[5]) { - for (int i = 0; i < 5; i++) { - testCaseStates[i] = states[i]; - } -} - -// Scan and update the test case LEDs; to be called periodically via timer or main loop -void scanTestCaseLEDs() { - for (int i = 0; i < 5; i++) { - uint8_t col = testCols[i]; - TestCaseState state = testCaseStates[i]; - - // Activate column - funDigitalWrite(col, FUN_HIGH); - - switch (state) { - case TC_NO_RESULT: - // Both rows OFF - funDigitalWrite(rowRed, FUN_HIGH); - funDigitalWrite(rowGreen, FUN_HIGH); - break; - case TC_PASS: - funDigitalWrite(rowRed, FUN_HIGH); - funDigitalWrite(rowGreen, FUN_LOW); - break; - - case TC_FAIL: - funDigitalWrite(rowRed, FUN_LOW); - funDigitalWrite(rowGreen, FUN_HIGH); - break; - - case TC_IN_PROGRESS: - // Alternate flashing: handled by timer toggling a flag - if (flashState) { - funDigitalWrite(rowRed, FUN_LOW); - funDigitalWrite(rowGreen, FUN_HIGH); - } else { - funDigitalWrite(rowRed, FUN_HIGH); - funDigitalWrite(rowGreen, FUN_LOW); - } - break; - - case TC_RETRY: - if (i == 4) { - funDigitalWrite(rowRed, flashState ? FUN_LOW : FUN_HIGH); - funDigitalWrite(rowGreen, FUN_HIGH); // Always OFF - } - break; - } - - Delay_Ms(2); // Short dwell time per column - - // Deactivate column - funDigitalWrite(col, FUN_LOW); - } + // RDY on + funDigitalWrite(PIN_RDY, FUN_LOW); - // Reset rows to avoid ghosting - funDigitalWrite(rowRed, FUN_HIGH); - funDigitalWrite(rowGreen, FUN_HIGH); + // Start timer and set IDLE flashing + setupStatusFlasher(); + runStatus(false); } +// -------------------- App logic -------------------- -void runTestCaseDemo() { - TestCaseState demoStates[5]; +static inline void runTestCaseDemo(void) { + static TestCaseState demoStates[5]; + // Pattern 1 demoStates[0] = TC_PASS; demoStates[1] = TC_FAIL; demoStates[2] = TC_IN_PROGRESS; demoStates[3] = TC_PASS; demoStates[4] = TC_RETRY; setTestCaseResult(demoStates); + waitTicks(5 * TICKS_PER_SECOND); - // Delay_Ms(5000); // Initial 2 second delay - // waitTicks(500); // 5 seconds - + // Pattern 2 demoStates[0] = TC_FAIL; demoStates[1] = TC_PASS; demoStates[2] = TC_FAIL; demoStates[3] = TC_RETRY; demoStates[4] = TC_IN_PROGRESS; setTestCaseResult(demoStates); - waitTicks(5); // 5 seconds - setTestCaseResult((TestCaseState[]){ TC_PASS, TC_PASS, TC_PASS, TC_PASS, TC_PASS } ); - setTestCaseResult((TestCaseState[]){ TC_FAIL, TC_FAIL, TC_FAIL, TC_FAIL, TC_FAIL } ); + waitTicks(5 * TICKS_PER_SECOND); + + // Pattern 3 (all pass) + for (int i = 0; i < 5; i++) demoStates[i] = TC_PASS; + setTestCaseResult(demoStates); + waitTicks(5 * TICKS_PER_SECOND); + + // Pattern 4 (all fail) + for (int i = 0; i < 5; i++) demoStates[i] = TC_FAIL; + setTestCaseResult(demoStates); } +// -------------------- Main -------------------- -int main() { +int main(void) { SystemInit(); - __enable_irq(); // Allow ISRs globally - - printf("Tester PCB Business Card Runtime\n"); setupPins(); - startupSequence(); - printf("Running test case demo\n"); - runTestCaseDemo(); - printf("Demo complete. Entering main loop.\n"); + setupStatusFlasher(); // start timer early for debug + + funDigitalWrite(PIN_ROW_G, FUN_LOW); // Active LOW + + funDigitalWrite(PIN_COL_A, FUN_HIGH); + funDigitalWrite(PIN_COL_B, FUN_HIGH); + funDigitalWrite(PIN_COL_C, FUN_HIGH); // active HIGH + funDigitalWrite(PIN_COL_D, FUN_HIGH); + funDigitalWrite(PIN_COL_E, FUN_HIGH); + + waitTicks(1000); + + funDigitalWrite(PIN_COL_A, FUN_LOW); + funDigitalWrite(PIN_COL_B, FUN_LOW); + funDigitalWrite(PIN_COL_C, FUN_LOW); // active HIGH + funDigitalWrite(PIN_COL_D, FUN_LOW); + funDigitalWrite(PIN_COL_E, FUN_LOW); + + waitTicks(1000); + + funDigitalWrite(PIN_COL_A, FUN_HIGH); + funDigitalWrite(PIN_COL_B, FUN_HIGH); + funDigitalWrite(PIN_COL_C, FUN_HIGH); // active HIGH + funDigitalWrite(PIN_COL_D, FUN_HIGH); + funDigitalWrite(PIN_COL_E, FUN_HIGH); + + waitTicks(1000); + + funDigitalWrite(PIN_COL_A, FUN_LOW); + funDigitalWrite(PIN_COL_B, FUN_LOW); + funDigitalWrite(PIN_COL_C, FUN_LOW); // active HIGH + funDigitalWrite(PIN_COL_D, FUN_LOW); + funDigitalWrite(PIN_COL_E, FUN_LOW); + + waitTicks(1000); + + funDigitalWrite(PIN_ROW_G, FUN_HIGH); // Turn off Green + funDigitalWrite(PIN_ROW_R, FUN_LOW); // Turn on Red + + funDigitalWrite(PIN_COL_A, FUN_HIGH); + funDigitalWrite(PIN_COL_B, FUN_HIGH); + funDigitalWrite(PIN_COL_C, FUN_HIGH); // active HIGH + funDigitalWrite(PIN_COL_D, FUN_HIGH); + funDigitalWrite(PIN_COL_E, FUN_HIGH); + + waitTicks(1000); + + funDigitalWrite(PIN_COL_A, FUN_LOW); + funDigitalWrite(PIN_COL_B, FUN_LOW); + funDigitalWrite(PIN_COL_C, FUN_LOW); // active HIGH + funDigitalWrite(PIN_COL_D, FUN_LOW); + funDigitalWrite(PIN_COL_E, FUN_LOW); + + waitTicks(1000); + + initTestCaseStates(); // publish valid buffer before timer starts + __enable_irq(); // enable global interrupts + startupSequence(); // starts TIM1 and sets IDLE mode + + // runTestCaseDemo(); + while (1) { - // Do main loop tasks here + serviceStatusLeds(); // update RUN/IDLE outside ISR + __WFI(); + static uint32_t lastBlink = 0; +if ((uint32_t)(msTicks - lastBlink) >= 200) { + lastBlink += 200; + static bool idleBlink = false; + idleBlink = !idleBlink; + funDigitalWrite(PIN_IDLE, idleBlink ? FUN_LOW : FUN_HIGH); // active LOW +} } } From 6ce6fa22d0acb08dfa5ec5a4d33b841a4f4aef3a Mon Sep 17 00:00:00 2001 From: Mark Benson Date: Fri, 5 Sep 2025 21:53:01 +0100 Subject: [PATCH 02/12] Working ISR Note: After a couple of days trying to track down hard faults, turns out it was down to the type of interrupt used. From what I understand "WCH-Interrupt-fast" is expecting the ISR to be stack safe - that is to say, don't call anything that touches the stack (library calls, functions, variables etc). Added the config entry to use stack safe ISR. --- .../firmware/tester_runtime/src/funconfig.h | 1 + software/firmware/tester_runtime/src/main.c | 301 ++++-------------- 2 files changed, 68 insertions(+), 234 deletions(-) diff --git a/software/firmware/tester_runtime/src/funconfig.h b/software/firmware/tester_runtime/src/funconfig.h index 7ca9cfd..8135df1 100644 --- a/software/firmware/tester_runtime/src/funconfig.h +++ b/software/firmware/tester_runtime/src/funconfig.h @@ -3,6 +3,7 @@ // you can put various configurations in here, you can see a full list in ch32fun.h // part selection is made in the Makefile +#define FUNCONF_ENABLE_HPE 0 #endif diff --git a/software/firmware/tester_runtime/src/main.c b/software/firmware/tester_runtime/src/main.c index 90be9d8..c2b1a90 100644 --- a/software/firmware/tester_runtime/src/main.c +++ b/software/firmware/tester_runtime/src/main.c @@ -71,6 +71,7 @@ static inline void gpio_clear(GPIO_TypeDef* p, uint16_t m) { p->BCR = m; } static const struct { GPIO_TypeDef* port; uint16_t mask; } COL[5] = { {GPIOC, BIT(0)}, {GPIOC, BIT(1)}, {GPIOC, BIT(2)}, {GPIOC, BIT(3)}, {GPIOC, BIT(4)} }; + static const struct { GPIO_TypeDef* port; uint16_t mask; } ROW_RED = { GPIOA, BIT(1) }; static const struct { GPIO_TypeDef* port; uint16_t mask; } ROW_GREEN = { GPIOA, BIT(2) }; @@ -85,20 +86,6 @@ void HardFault_Handler(void) for (;;) { /* spin */ } } -// void HardFault_Handler(void) -// { -// asm volatile ("csrr %0, mcause" : "=r"(last_mcause)); -// asm volatile ("csrr %0, mepc" : "=r"(last_mepc)); - -// // Rapid flash INIT so it's visible -// while (1) { -// funDigitalWrite(PIN_INIT, FUN_LOW); -// for (volatile int i = 0; i < 60000; i++) {}; -// funDigitalWrite(PIN_INIT, FUN_HIGH); -// for (volatile int i = 0; i < 60000; i++) {}; -// } -// } - // -------------------- Helpers -------------------- static inline void initTestCaseStates(void) { @@ -121,128 +108,67 @@ static inline void setTestCaseResult(const TestCaseState states[5]) { __enable_irq(); } +static inline void scanStep(void) { + static int8_t prev = -1; // last active column, -1 = none yet + + // 0. Bail out early if state pointer is invalid + volatile const TestCaseState* states = activeStates; + if (states == NULL) { + // Ensure all LEDs off + gpio_set(ROW_RED.port, ROW_RED.mask); + gpio_set(ROW_GREEN.port, ROW_GREEN.mask); + return; + } -// static inline void scanStep(void) { -// static bool rdy = false; -// rdy = !rdy; -// funDigitalWrite(PIN_RDY, rdy ? FUN_LOW : FUN_HIGH); // active LOW -// } + // 1. Turn both rows OFF (active LOW → drive HIGH) before touching columns + gpio_set(ROW_RED.port, ROW_RED.mask); + gpio_set(ROW_GREEN.port, ROW_GREEN.mask); -// static inline void scanStep(void) { -// gpio_set(COL[0].port, COL[0].mask); // active HIGH -// } + // 2. Clear the previous column if it was valid + if (prev >= 0 && prev < 5) { + if (COL[prev].port != NULL) { + gpio_clear(COL[prev].port, COL[prev].mask); + } + } -static inline void scanStep(void) { - gpio_set(COL[1].port, COL[1].mask); - gpio_clear(COL[1].port, COL[1].mask); -} + // 3. Decide what to light for the current column (only if in range) + if (currentCol >= 0 && currentCol < 5) { + TestCaseState state = states[currentCol]; + + switch (state) { + case TC_PASS: + gpio_clear(ROW_GREEN.port, ROW_GREEN.mask); // green ON + break; + case TC_FAIL: + gpio_clear(ROW_RED.port, ROW_RED.mask); // red ON + break; + case TC_IN_PROGRESS: + if (flashState) + gpio_clear(ROW_RED.port, ROW_RED.mask); + else + gpio_clear(ROW_GREEN.port, ROW_GREEN.mask); + break; + case TC_RETRY: + if (flashState) + gpio_clear(ROW_RED.port, ROW_RED.mask); + break; + case TC_NO_RESULT: + default: + // both rows stay off + break; + } + // 4. Set the current column HIGH (active HIGH) + if (COL[currentCol].port != NULL) { + gpio_set(COL[currentCol].port, COL[currentCol].mask); + } + } + + // 5. Advance indices for next tick + prev = currentCol; + currentCol = (currentCol + 1) % 5; +} -// Phased probe: one write per tick to find the wedging operation -// static inline void scanStep(void) { -// static uint8_t phase = 0; // advances 0..7 repeatedly -// static int8_t prev = -1; // previous column index -// static uint8_t col = 0; // current column index (0..4) - -// // Show ISR progress by ticking RDY each tick (toggle) -// static bool rdy = false; -// rdy = !rdy; -// funDigitalWrite(PIN_RDY, rdy ? FUN_LOW : FUN_HIGH); // active LOW - -// switch (phase) { -// case 0: -// // 0) rows OFF (active LOW -> set HIGH) -// gpio_set(ROW_RED.port, ROW_RED.mask); -// break; - -// case 1: -// // 1) rows OFF (green) -// gpio_set(ROW_GREEN.port, ROW_GREEN.mask); -// break; - -// case 2: -// // 2) turn OFF previous column (if any) -// if (prev >= 0) gpio_clear(COL[prev].port, COL[prev].mask); -// break; - -// case 3: -// // 3) drive rows for current column: PASS => green ON (LOW) -// gpio_clear(ROW_GREEN.port, ROW_GREEN.mask); -// break; - -// case 4: -// // 4) turn ON current column (active HIGH) -// gpio_set(COL[col].port, COL[col].mask); -// break; - -// case 5: -// // 5) do nothing (gap) -// break; - -// case 6: -// // 6) advance column index -// prev = col; -// col = (uint8_t)((col + 1) % 5); -// break; - -// case 7: -// // 7) do nothing (gap) -// break; -// } - -// // Advance to next phase each tick -// phase = (uint8_t)((phase + 1) & 7); -// } - - -// static inline void scanStep(void) { -// static int8_t prevCol = -1; - -// // Turn off previous column (active HIGH -> write LOW) -// if (prevCol >= 0) { -// // funDigitalWrite(testCols[prevCol], FUN_LOW); -// } - -// // Default rows OFF (active LOW -> write HIGH) -// // funDigitalWrite(PIN_ROW_R, FUN_HIGH); -// // funDigitalWrite(PIN_ROW_G, FUN_HIGH); - -// // Drive rows for this column based on state -// const volatile TestCaseState* states = activeStates; -// TestCaseState state = states[currentCol]; - -// switch (state) { -// case TC_NO_RESULT: -// // both OFF -// break; -// case TC_PASS: -// // funDigitalWrite(PIN_ROW_G, FUN_LOW); -// break; -// case TC_FAIL: -// // funDigitalWrite(PIN_ROW_R, FUN_LOW); -// break; -// case TC_IN_PROGRESS: -// if (flashState) { -// // funDigitalWrite(PIN_ROW_R, FUN_LOW); -// } -// else { -// // funDigitalWrite(PIN_ROW_G, FUN_LOW); -// } -// break; -// case TC_RETRY: -// if (currentCol == 4) { // only TC5 flashes red -// // if (flashState) funDigitalWrite(PIN_ROW_R, FUN_LOW); -// } -// break; -// } - -// // Turn on this column last (active HIGH) -// // funDigitalWrite(testCols[currentCol], FUN_HIGH); - -// // Advance -// prevCol = currentCol; -// currentCol = (currentCol + 1) % 5; -// } static inline void waitTicks(uint32_t ticks) { uint32_t start = msTicks; @@ -266,23 +192,19 @@ static inline void serviceStatusLeds(void) { // -------------------- Timer ISR -------------------- -void TIM1_UP_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast"))); +void TIM1_UP_IRQHandler(void) INTERRUPT_DECORATOR; void TIM1_UP_IRQHandler(void) { - if (TIM1->INTFR & TIM_UIF) - { - // Clear update flag only + if (TIM1->INTFR & TIM_UIF) { TIM1->INTFR &= ~TIM_UIF; - msTicks++; - scanStep(); - // Flip flashState every 250 ms for TC blinking static uint32_t nextFlashAt = 250; if (msTicks >= nextFlashAt) { nextFlashAt += 250; flashState = !flashState; } + scanStep(); } } @@ -345,38 +267,6 @@ static inline void setupPins(void) { } } - - -// static inline void setupPins(void) { -// funGpioInitAll(); -// RCC->APB2PCENR |= RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD; - - -// // Status LEDs (active LOW) -// for (int i = 0; i < 5; i++) { -// funPinMode(statusLEDs[i], GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); -// funDigitalWrite(statusLEDs[i], FUN_HIGH); // OFF -// } - -// // Test case columns (active HIGH) -// for (int i = 0; i < 5; i++) { -// funPinMode(testCols[i], GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); -// funDigitalWrite(testCols[i], FUN_LOW); // start OFF -// } - -// // Test case rows (active LOW -> set HIGH to turn OFF) -// for (int i = 0; i < 2; i++) { -// funPinMode(testRows[i], GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); -// funDigitalWrite(testRows[i], FUN_HIGH); -// } - -// // Inputs with pull-ups -// for (int i = 0; i < 4; i++) { -// funPinMode(inputPins[i], GPIO_CNF_IN_PUPD); -// funDigitalWrite(inputPins[i], FUN_HIGH); -// } -// } - static inline void startupSequence(void) { funDigitalWrite(PIN_PWR, FUN_LOW); // PWR stays on @@ -393,6 +283,7 @@ static inline void startupSequence(void) { // Start timer and set IDLE flashing setupStatusFlasher(); + __enable_irq(); // enable global interrupts runStatus(false); } @@ -434,77 +325,19 @@ static inline void runTestCaseDemo(void) { int main(void) { SystemInit(); setupPins(); - setupStatusFlasher(); // start timer early for debug - - funDigitalWrite(PIN_ROW_G, FUN_LOW); // Active LOW - - funDigitalWrite(PIN_COL_A, FUN_HIGH); - funDigitalWrite(PIN_COL_B, FUN_HIGH); - funDigitalWrite(PIN_COL_C, FUN_HIGH); // active HIGH - funDigitalWrite(PIN_COL_D, FUN_HIGH); - funDigitalWrite(PIN_COL_E, FUN_HIGH); - - waitTicks(1000); - - funDigitalWrite(PIN_COL_A, FUN_LOW); - funDigitalWrite(PIN_COL_B, FUN_LOW); - funDigitalWrite(PIN_COL_C, FUN_LOW); // active HIGH - funDigitalWrite(PIN_COL_D, FUN_LOW); - funDigitalWrite(PIN_COL_E, FUN_LOW); - - waitTicks(1000); - - funDigitalWrite(PIN_COL_A, FUN_HIGH); - funDigitalWrite(PIN_COL_B, FUN_HIGH); - funDigitalWrite(PIN_COL_C, FUN_HIGH); // active HIGH - funDigitalWrite(PIN_COL_D, FUN_HIGH); - funDigitalWrite(PIN_COL_E, FUN_HIGH); - - waitTicks(1000); - - funDigitalWrite(PIN_COL_A, FUN_LOW); - funDigitalWrite(PIN_COL_B, FUN_LOW); - funDigitalWrite(PIN_COL_C, FUN_LOW); // active HIGH - funDigitalWrite(PIN_COL_D, FUN_LOW); - funDigitalWrite(PIN_COL_E, FUN_LOW); - - waitTicks(1000); - - funDigitalWrite(PIN_ROW_G, FUN_HIGH); // Turn off Green - funDigitalWrite(PIN_ROW_R, FUN_LOW); // Turn on Red - - funDigitalWrite(PIN_COL_A, FUN_HIGH); - funDigitalWrite(PIN_COL_B, FUN_HIGH); - funDigitalWrite(PIN_COL_C, FUN_HIGH); // active HIGH - funDigitalWrite(PIN_COL_D, FUN_HIGH); - funDigitalWrite(PIN_COL_E, FUN_HIGH); - - waitTicks(1000); - - funDigitalWrite(PIN_COL_A, FUN_LOW); - funDigitalWrite(PIN_COL_B, FUN_LOW); - funDigitalWrite(PIN_COL_C, FUN_LOW); // active HIGH - funDigitalWrite(PIN_COL_D, FUN_LOW); - funDigitalWrite(PIN_COL_E, FUN_LOW); - - waitTicks(1000); initTestCaseStates(); // publish valid buffer before timer starts - __enable_irq(); // enable global interrupts + startupSequence(); // starts TIM1 and sets IDLE mode - // runTestCaseDemo(); + waitTicks(1000); // let it settle + + runTestCaseDemo(); while (1) { serviceStatusLeds(); // update RUN/IDLE outside ISR __WFI(); - static uint32_t lastBlink = 0; -if ((uint32_t)(msTicks - lastBlink) >= 200) { - lastBlink += 200; - static bool idleBlink = false; - idleBlink = !idleBlink; - funDigitalWrite(PIN_IDLE, idleBlink ? FUN_LOW : FUN_HIGH); // active LOW -} + } - } } + From e91f5ce6c3cc0fee05fdb99364fb6f507724bc43 Mon Sep 17 00:00:00 2001 From: Mark Benson Date: Sun, 7 Sep 2025 13:51:25 +0100 Subject: [PATCH 03/12] Fix button input state config --- software/firmware/tester_runtime/src/main.c | 92 ++++++++++++++++++++- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/software/firmware/tester_runtime/src/main.c b/software/firmware/tester_runtime/src/main.c index c2b1a90..2062410 100644 --- a/software/firmware/tester_runtime/src/main.c +++ b/software/firmware/tester_runtime/src/main.c @@ -40,6 +40,17 @@ static const uint16_t inputPins[] = { PIN_INPUT_A, PIN_INPUT_B, PIN_INPUT_C, PI // -------------------- State and flags -------------------- +typedef struct { + uint32_t lastPressTime; + uint32_t lastReleaseTime; + uint16_t pressCount; + bool pressed; +} ButtonState; + +volatile ButtonState buttons[4]; +static uint8_t lastButtonSample = 0xFF; // all released (active-low) + + typedef enum { TC_NO_RESULT, TC_PASS, @@ -66,6 +77,9 @@ volatile uint32_t last_mcause = 0, last_mepc = 0; #define BIT(n) (1U << (n)) static inline void gpio_set(GPIO_TypeDef* p, uint16_t m) { p->BSHR = m; } static inline void gpio_clear(GPIO_TypeDef* p, uint16_t m) { p->BCR = m; } +static inline uint8_t gpio_get(GPIO_TypeDef* p, uint16_t m) { + return (p->INDR & m) ? 1U : 0U; +} // --- Explicit port/mask map for columns and rows --- static const struct { GPIO_TypeDef* port; uint16_t mask; } COL[5] = { @@ -75,6 +89,11 @@ static const struct { GPIO_TypeDef* port; uint16_t mask; } COL[5] = { static const struct { GPIO_TypeDef* port; uint16_t mask; } ROW_RED = { GPIOA, BIT(1) }; static const struct { GPIO_TypeDef* port; uint16_t mask; } ROW_GREEN = { GPIOA, BIT(2) }; +// port/mask map for input buttons +static const struct { GPIO_TypeDef* port; uint16_t mask; } BTN[4] = { + {GPIOD, BIT(3)}, {GPIOD, BIT(2)}, {GPIOD, BIT(4)}, {GPIOD, BIT(5)} +}; + // -------------------- Hard fault handler -------------------- @@ -190,6 +209,45 @@ static inline void serviceStatusLeds(void) { } } +static uint32_t initLedOffAt = 0; + +static inline void pollButtons(void) { + uint8_t sample = 0; + for (int i = 0; i < 4; i++) { + if (!gpio_get(BTN[i].port, BTN[i].mask)) { + sample |= (1 << i); + } + } + + uint8_t changed = sample ^ lastButtonSample; + if (changed) { + for (int i = 0; i < 4; i++) { + if ((changed & (1 << i)) && (sample & (1 << i))) { + // Pressed + buttons[i].pressed = true; + buttons[i].lastPressTime = msTicks; + buttons[i].pressCount++; + + // Start INIT LED pulse + gpio_clear(GPIOC, BIT(6)); // INIT LED on (active low) + initLedOffAt = msTicks + 50; // 50 ms pulse + } else if (changed & (1 << i)) { + // Released + buttons[i].pressed = false; + buttons[i].lastReleaseTime = msTicks; + } + } + } + + // End pulse if time elapsed + if (initLedOffAt && msTicks >= initLedOffAt) { + gpio_set(GPIOC, BIT(6)); // INIT LED off + initLedOffAt = 0; + } + + lastButtonSample = sample; +} + // -------------------- Timer ISR -------------------- void TIM1_UP_IRQHandler(void) INTERRUPT_DECORATOR; @@ -198,13 +256,14 @@ void TIM1_UP_IRQHandler(void) if (TIM1->INTFR & TIM_UIF) { TIM1->INTFR &= ~TIM_UIF; msTicks++; - + pollButtons(); static uint32_t nextFlashAt = 250; if (msTicks >= nextFlashAt) { nextFlashAt += 250; flashState = !flashState; } scanStep(); + } } @@ -267,6 +326,29 @@ static inline void setupPins(void) { } } +static inline void testCaseLEDStartupPattern(void) { + static TestCaseState demoStates[5]; + + // Repeat the above 3 times so it looks like a "chase" + for (int k = 0; k < 4; k++) { + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 5; j++) demoStates[j] = TC_NO_RESULT; + demoStates[i] = TC_PASS; + setTestCaseResult(demoStates); + waitTicks(30); + } + for (int i = 4; i >= 0; i--) { + for (int j = 0; j < 5; j++) demoStates[j] = TC_NO_RESULT; + demoStates[i] = TC_FAIL; + setTestCaseResult(demoStates); + waitTicks(30); + } + } + // end with all off + for (int j = 0; j < 5; j++) demoStates[j] = TC_NO_RESULT; + setTestCaseResult(demoStates); +} + static inline void startupSequence(void) { funDigitalWrite(PIN_PWR, FUN_LOW); // PWR stays on @@ -285,10 +367,12 @@ static inline void startupSequence(void) { setupStatusFlasher(); __enable_irq(); // enable global interrupts runStatus(false); + testCaseLEDStartupPattern(); } // -------------------- App logic -------------------- + static inline void runTestCaseDemo(void) { static TestCaseState demoStates[5]; @@ -324,15 +408,17 @@ static inline void runTestCaseDemo(void) { int main(void) { SystemInit(); + setupPins(); initTestCaseStates(); // publish valid buffer before timer starts startupSequence(); // starts TIM1 and sets IDLE mode - waitTicks(1000); // let it settle + waitTicks(100); // let it settle - runTestCaseDemo(); + // runTestCaseDemo(); + // testCaseLEDStartupPattern(); while (1) { serviceStatusLeds(); // update RUN/IDLE outside ISR From e969f62bf2c6b4e01c8750f88020dda298ac83de Mon Sep 17 00:00:00 2001 From: Mark Benson Date: Sun, 7 Sep 2025 13:51:44 +0100 Subject: [PATCH 04/12] Fix input button config --- software/firmware/board_check/src/board_check.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/software/firmware/board_check/src/board_check.c b/software/firmware/board_check/src/board_check.c index ea2a943..1d0a34e 100644 --- a/software/firmware/board_check/src/board_check.c +++ b/software/firmware/board_check/src/board_check.c @@ -31,7 +31,8 @@ const uint8_t statusLEDs[] = { PIN_INIT, PIN_RDY, PIN_RUN, PIN_IDLE }; const uint8_t cols[] = { PIN_COL_A, PIN_COL_B, PIN_COL_C, PIN_COL_D, PIN_COL_E }; - +const uint8_t inputPins[] = { PIN_INPUT_A, PIN_INPUT_B, PIN_INPUT_C, PIN_INPUT_D }; +const uint8_t inputCols[] = { PIN_COL_A, PIN_COL_B, PIN_COL_C, PIN_COL_D }; // TC1–TC4 typedef enum { MODE_DEMO, MODE_INPUT @@ -62,10 +63,13 @@ void setupPins() { funPinMode(PIN_ROW_G, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); funDigitalWrite(PIN_ROW_R, FUN_HIGH); // Start rows OFF funDigitalWrite(PIN_ROW_G, FUN_HIGH); + + for (int i = 0; i < 4; i++) { + funPinMode(inputPins[i], GPIO_CNF_IN_PUPD); + funDigitalWrite(inputPins[i], FUN_HIGH); // enable pull‑up } -const uint8_t inputPins[] = { PIN_INPUT_A, PIN_INPUT_B, PIN_INPUT_C, PIN_INPUT_D }; -const uint8_t inputCols[] = { PIN_COL_A, PIN_COL_B, PIN_COL_C, PIN_COL_D }; // TC1–TC4 +} bool isAnyButtonPressed() { for (int i = 0; i < 4; i++) { From cb2bd16703e97fea116585fd8b05561ea5ec7161 Mon Sep 17 00:00:00 2001 From: Mark Benson Date: Sun, 7 Sep 2025 14:13:01 +0100 Subject: [PATCH 05/12] Basic test case runner --- software/firmware/tester_runtime/src/main.c | 76 ++++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/software/firmware/tester_runtime/src/main.c b/software/firmware/tester_runtime/src/main.c index 2062410..15ec64a 100644 --- a/software/firmware/tester_runtime/src/main.c +++ b/software/firmware/tester_runtime/src/main.c @@ -370,9 +370,6 @@ static inline void startupSequence(void) { testCaseLEDStartupPattern(); } -// -------------------- App logic -------------------- - - static inline void runTestCaseDemo(void) { static TestCaseState demoStates[5]; @@ -404,6 +401,71 @@ static inline void runTestCaseDemo(void) { setTestCaseResult(demoStates); } +// -------------------- App logic -------------------- + +static TestCaseState tcResults[5] = { TC_NO_RESULT }; + +typedef struct { + uint32_t durationMs; // fixed duration + bool (*evalFn)(void); // returns pass/fail +} TestCaseDef; + +static uint8_t currentTest = 0; +static uint32_t testStartTime = 0; +static bool testActive = false; + +// For simple “all A–C pressed” tracking +static bool seenA = false, seenB = false, seenC = false; + +static bool eval_all_ABC(void) { + return seenA && seenB && seenC; +} + +static const TestCaseDef testCases[4] = { + { 5000, eval_all_ABC }, // 5s test + { 5000, eval_all_ABC }, // placeholder + { 5000, eval_all_ABC }, // placeholder + { 5000, eval_all_ABC } // placeholder +}; + +static void startTest(uint8_t idx) { + currentTest = idx; + testStartTime = msTicks; + testActive = true; + seenA = seenB = seenC = false; + + runStatus(true); // RUN LED + TestCaseState states[5] = { TC_NO_RESULT }; + states[idx] = TC_IN_PROGRESS; + setTestCaseResult(states); +} + +static void endTest(void) { + testActive = false; + runStatus(false); // IDLE LED + + bool pass = testCases[currentTest].evalFn(); + TestCaseState states[5] = { TC_NO_RESULT }; + states[currentTest] = pass ? TC_PASS : TC_FAIL; + setTestCaseResult(states); + + // Advance to next test (wrap after 4) + currentTest = (currentTest + 1) % 4; +} + +static void monitorInputs(void) { + if (!testActive) return; + + if (buttons[0].pressed) seenA = true; // BTN A + if (buttons[1].pressed) seenB = true; // BTN B + if (buttons[2].pressed) seenC = true; // BTN C + + if ((uint32_t)(msTicks - testStartTime) >= testCases[currentTest].durationMs) { + endTest(); + } +} + + // -------------------- Main -------------------- int main(void) { @@ -422,6 +484,14 @@ int main(void) { while (1) { serviceStatusLeds(); // update RUN/IDLE outside ISR + // START TEST button = BTN D (index 3) + + if (!testActive && buttons[3].pressed) { + startTest(currentTest); + } + + monitorInputs(); + __WFI(); } From 7a748641c698d1ad0842b146c8925088cbfa6cd9 Mon Sep 17 00:00:00 2001 From: Mark Benson Date: Sun, 7 Sep 2025 20:23:21 +0100 Subject: [PATCH 06/12] Add TC1, TC2 & TC3 --- software/firmware/tester_runtime/src/main.c | 174 ++++++++++++++++---- 1 file changed, 138 insertions(+), 36 deletions(-) diff --git a/software/firmware/tester_runtime/src/main.c b/software/firmware/tester_runtime/src/main.c index 15ec64a..b3b611c 100644 --- a/software/firmware/tester_runtime/src/main.c +++ b/software/firmware/tester_runtime/src/main.c @@ -44,10 +44,12 @@ typedef struct { uint32_t lastPressTime; uint32_t lastReleaseTime; uint16_t pressCount; - bool pressed; + bool pressed; // true while button is physically held down + bool justPressed; // true only on the tick a press is first detected, until consumed } ButtonState; volatile ButtonState buttons[4]; + static uint8_t lastButtonSample = 0xFF; // all released (active-low) @@ -91,7 +93,7 @@ static const struct { GPIO_TypeDef* port; uint16_t mask; } ROW_GREEN = { GPIOA, // port/mask map for input buttons static const struct { GPIO_TypeDef* port; uint16_t mask; } BTN[4] = { - {GPIOD, BIT(3)}, {GPIOD, BIT(2)}, {GPIOD, BIT(4)}, {GPIOD, BIT(5)} + {GPIOD, BIT(2)}, {GPIOD, BIT(3)}, {GPIOD, BIT(4)}, {GPIOD, BIT(5)} }; @@ -223,23 +225,25 @@ static inline void pollButtons(void) { if (changed) { for (int i = 0; i < 4; i++) { if ((changed & (1 << i)) && (sample & (1 << i))) { - // Pressed - buttons[i].pressed = true; + // Rising edge: Pressed + buttons[i].pressed = true; // level + buttons[i].justPressed = true; // new: one-shot event buttons[i].lastPressTime = msTicks; buttons[i].pressCount++; // Start INIT LED pulse gpio_clear(GPIOC, BIT(6)); // INIT LED on (active low) initLedOffAt = msTicks + 50; // 50 ms pulse + } else if (changed & (1 << i)) { - // Released + // Falling edge: Released buttons[i].pressed = false; buttons[i].lastReleaseTime = msTicks; } } } - // End pulse if time elapsed + // End INIT LED pulse if time elapsed if (initLedOffAt && msTicks >= initLedOffAt) { gpio_set(GPIOC, BIT(6)); // INIT LED off initLedOffAt = 0; @@ -404,68 +408,166 @@ static inline void runTestCaseDemo(void) { // -------------------- App logic -------------------- static TestCaseState tcResults[5] = { TC_NO_RESULT }; - typedef struct { - uint32_t durationMs; // fixed duration - bool (*evalFn)(void); // returns pass/fail + uint32_t durationMs; + void (*initFn)(void); + void (*updateFn)(void); + TestCaseState (*evalFn)(void); } TestCaseDef; + static uint8_t currentTest = 0; static uint32_t testStartTime = 0; static bool testActive = false; -// For simple “all A–C pressed” tracking -static bool seenA = false, seenB = false, seenC = false; +// ===== TC1: All A–C pressed ===== +static bool seenA, seenB, seenC; + +static void tc1_init(void) { + seenA = seenB = seenC = false; +} + +static void tc1_update(void) { + if (buttons[0].pressed) seenA = true; + if (buttons[1].pressed) seenB = true; + if (buttons[2].pressed) seenC = true; +} + +static TestCaseState tc1_eval(void) { + return (seenA && seenB && seenC) ? TC_PASS : TC_FAIL; +} + +// ===== TC2: Debounce timing ===== +static uint32_t firstPressTimeB, lastPressTimeB; +static uint8_t pressCountB; +static const uint32_t DEBOUNCE_THRESHOLD_MS = 200; + +// TC2: Fast double-press detection +static uint32_t pressTimes[4]; +static uint8_t pressIndex; +static const uint32_t FAST_THRESHOLD_MS = 200; +static const uint32_t MIN_GAP_MS = 30; // reject bounce -static bool eval_all_ABC(void) { - return seenA && seenB && seenC; +static void tc2_init(void) { + pressIndex = 0; } -static const TestCaseDef testCases[4] = { - { 5000, eval_all_ABC }, // 5s test - { 5000, eval_all_ABC }, // placeholder - { 5000, eval_all_ABC }, // placeholder - { 5000, eval_all_ABC } // placeholder +static void tc2_update(void) { + if (buttons[1].justPressed) { + buttons[1].justPressed = false; // consume event + if (pressIndex < 4) { + pressTimes[pressIndex++] = msTicks; + } + } +} + +static TestCaseState tc2_eval(void) { + if (pressCountB < 2) { + return TC_FAIL; // no presses or only one press + } + + uint32_t gap = lastPressTimeB - firstPressTimeB; + + if (gap < MIN_GAP_MS) { + return TC_FAIL; // bounce + } + if (gap >= FAST_THRESHOLD_MS) { + return TC_FAIL; // too slow + } + + return TC_PASS; // valid fast double press +} + + +// ===== TC3: Conditional Logic – Decision Table Test ===== + +// Latch states for A, B, C during the 5s window +static bool seenA3, seenB3, seenC3; + +static void tc3_init(void) { + seenA3 = seenB3 = seenC3 = false; +} + +static void tc3_update(void) { + if (buttons[0].justPressed) { + buttons[0].justPressed = false; + seenA3 = !seenA3; // toggle + } + if (buttons[1].justPressed) { + buttons[1].justPressed = false; + seenB3 = !seenB3; // toggle + } + if (buttons[2].justPressed) { + buttons[2].justPressed = false; + seenC3 = !seenC3; // toggle + } +} + + +// Decision table returning your existing TestCaseState +static TestCaseState tc3_eval(void) { + // Decision table mapping + if (!seenA3 && !seenB3 && !seenC3) return TC_FAIL; // 000 + if (!seenA3 && !seenB3 && seenC3) return TC_RETRY; // 001 + if (!seenA3 && seenB3 && !seenC3) return TC_RETRY; // 010 + if (!seenA3 && seenB3 && seenC3) return TC_PASS; // 011 ✅ + if ( seenA3 && !seenB3 && !seenC3) return TC_FAIL; // 100 + if ( seenA3 && !seenB3 && seenC3) return TC_RETRY; // 101 + if ( seenA3 && seenB3 && !seenC3) return TC_RETRY; // 110 + if ( seenA3 && seenB3 && seenC3) return TC_RETRY; // 111 + return TC_FAIL; +} + + +// ===== Test case framework ===== + + +static const TestCaseDef testCases[] = { + { 5000, tc1_init, tc1_update, tc1_eval }, + { 5000, tc2_init, tc2_update, tc2_eval }, + { 5000, tc3_init, tc3_update, tc3_eval }, }; +static const size_t NUM_TEST_CASES = sizeof(testCases) / sizeof(testCases[0]); + static void startTest(uint8_t idx) { - currentTest = idx; + if (idx == 0) { + for (size_t i = 0; i < NUM_TEST_CASES; i++) { + tcResults[i] = TC_NO_RESULT; + } + } testStartTime = msTicks; testActive = true; - seenA = seenB = seenC = false; - runStatus(true); // RUN LED - TestCaseState states[5] = { TC_NO_RESULT }; - states[idx] = TC_IN_PROGRESS; - setTestCaseResult(states); + runStatus(true); + tcResults[idx] = TC_IN_PROGRESS; + setTestCaseResult(tcResults); + + testCases[idx].initFn(); // per-test init } static void endTest(void) { testActive = false; - runStatus(false); // IDLE LED + runStatus(false); - bool pass = testCases[currentTest].evalFn(); - TestCaseState states[5] = { TC_NO_RESULT }; - states[currentTest] = pass ? TC_PASS : TC_FAIL; - setTestCaseResult(states); + TestCaseState outcome = testCases[currentTest].evalFn(); + tcResults[currentTest] = outcome; + setTestCaseResult(tcResults); - // Advance to next test (wrap after 4) - currentTest = (currentTest + 1) % 4; + currentTest = (currentTest + 1) % NUM_TEST_CASES; } static void monitorInputs(void) { if (!testActive) return; - - if (buttons[0].pressed) seenA = true; // BTN A - if (buttons[1].pressed) seenB = true; // BTN B - if (buttons[2].pressed) seenC = true; // BTN C - + testCases[currentTest].updateFn(); // per-test input handling if ((uint32_t)(msTicks - testStartTime) >= testCases[currentTest].durationMs) { endTest(); } } + + // -------------------- Main -------------------- int main(void) { From ad25117946e61c1282385893d46af2ce11eb8c0f Mon Sep 17 00:00:00 2001 From: Mark Benson Date: Sun, 7 Sep 2025 20:34:56 +0100 Subject: [PATCH 07/12] Add TC4 --- software/firmware/tester_runtime/src/main.c | 55 +++++++++++++++++++-- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/software/firmware/tester_runtime/src/main.c b/software/firmware/tester_runtime/src/main.c index b3b611c..a6476d3 100644 --- a/software/firmware/tester_runtime/src/main.c +++ b/software/firmware/tester_runtime/src/main.c @@ -519,6 +519,56 @@ static TestCaseState tc3_eval(void) { } +// ===== TC4: Input Range Check ===== + +// Press counters for A, B, C +static uint8_t countA4, countB4, countC4; + +// Valid ranges +#define A_MIN 1 +#define A_MAX 3 +#define B_MIN 5 +#define B_MAX 7 +#define C_MIN 2 +#define C_MAX 4 + +static void tc4_init(void) { + countA4 = countB4 = countC4 = 0; +} + +static void tc4_update(void) { + if (buttons[0].justPressed) { buttons[0].justPressed = false; countA4++; } + if (buttons[1].justPressed) { buttons[1].justPressed = false; countB4++; } + if (buttons[2].justPressed) { buttons[2].justPressed = false; countC4++; } +} + +static TestCaseState tc4_eval(void) { + bool validA = (countA4 >= A_MIN && countA4 <= A_MAX); + bool validB = (countB4 >= B_MIN && countB4 <= B_MAX); + bool validC = (countC4 >= C_MIN && countC4 <= C_MAX); + + bool anyValid = validA || validB || validC; + + // For now, map to overall PASS/FAIL/RETRY + // You could extend this to show per‑input LEDs if hardware supports it + if (!anyValid) { + return TC_FAIL; + } + + // Optional: treat borderline values (exactly at min or max) as RETRY + bool borderlineA = validA && (countA4 == A_MIN || countA4 == A_MAX); + bool borderlineB = validB && (countB4 == B_MIN || countB4 == B_MAX); + bool borderlineC = validC && (countC4 == C_MIN || countC4 == C_MAX); + + if (borderlineA || borderlineB || borderlineC) { + return TC_RETRY; // blink pattern for borderline + } + + return TC_PASS; +} + + + // ===== Test case framework ===== @@ -526,6 +576,8 @@ static const TestCaseDef testCases[] = { { 5000, tc1_init, tc1_update, tc1_eval }, { 5000, tc2_init, tc2_update, tc2_eval }, { 5000, tc3_init, tc3_update, tc3_eval }, + { 5000, tc4_init, tc4_update, tc4_eval }, + }; static const size_t NUM_TEST_CASES = sizeof(testCases) / sizeof(testCases[0]); @@ -565,9 +617,6 @@ static void monitorInputs(void) { } } - - - // -------------------- Main -------------------- int main(void) { From 859386a1099533287e3e5410836531f58a8d4a7d Mon Sep 17 00:00:00 2001 From: Mark Benson Date: Sun, 7 Sep 2025 20:57:46 +0100 Subject: [PATCH 08/12] Differentiate between retry and warning state --- software/firmware/tester_runtime/src/main.c | 28 +++++++++++++-------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/software/firmware/tester_runtime/src/main.c b/software/firmware/tester_runtime/src/main.c index a6476d3..dd8e4a6 100644 --- a/software/firmware/tester_runtime/src/main.c +++ b/software/firmware/tester_runtime/src/main.c @@ -57,12 +57,14 @@ typedef enum { TC_NO_RESULT, TC_PASS, TC_FAIL, + TC_WARNING, TC_IN_PROGRESS, TC_RETRY } TestCaseState; volatile bool runMode = false; // false = IDLE, true = RUN volatile bool flashState = false; // toggles every 250 ms +volatile bool rapidFlashState = false; // toggles every 125 ms volatile uint32_t msTicks = 0; // monotonic ms counter volatile uint8_t currentCol = 0; // scan index @@ -169,10 +171,14 @@ static inline void scanStep(void) { else gpio_clear(ROW_GREEN.port, ROW_GREEN.mask); break; - case TC_RETRY: + case TC_WARNING: if (flashState) gpio_clear(ROW_RED.port, ROW_RED.mask); break; + case TC_RETRY: + if (rapidFlashState) + gpio_clear(ROW_RED.port, ROW_RED.mask); + break; case TC_NO_RESULT: default: // both rows stay off @@ -265,6 +271,11 @@ void TIM1_UP_IRQHandler(void) if (msTicks >= nextFlashAt) { nextFlashAt += 250; flashState = !flashState; + } + static uint32_t nextRapidFlashAt = 125; + if (msTicks >= nextRapidFlashAt) { + nextRapidFlashAt += 125; + rapidFlashState = !rapidFlashState; } scanStep(); @@ -508,13 +519,13 @@ static void tc3_update(void) { static TestCaseState tc3_eval(void) { // Decision table mapping if (!seenA3 && !seenB3 && !seenC3) return TC_FAIL; // 000 - if (!seenA3 && !seenB3 && seenC3) return TC_RETRY; // 001 - if (!seenA3 && seenB3 && !seenC3) return TC_RETRY; // 010 + if (!seenA3 && !seenB3 && seenC3) return TC_WARNING; // 001 + if (!seenA3 && seenB3 && !seenC3) return TC_WARNING; // 010 if (!seenA3 && seenB3 && seenC3) return TC_PASS; // 011 ✅ if ( seenA3 && !seenB3 && !seenC3) return TC_FAIL; // 100 - if ( seenA3 && !seenB3 && seenC3) return TC_RETRY; // 101 - if ( seenA3 && seenB3 && !seenC3) return TC_RETRY; // 110 - if ( seenA3 && seenB3 && seenC3) return TC_RETRY; // 111 + if ( seenA3 && !seenB3 && seenC3) return TC_WARNING; // 101 + if ( seenA3 && seenB3 && !seenC3) return TC_WARNING; // 110 + if ( seenA3 && seenB3 && seenC3) return TC_WARNING; // 111 return TC_FAIL; } @@ -561,14 +572,12 @@ static TestCaseState tc4_eval(void) { bool borderlineC = validC && (countC4 == C_MIN || countC4 == C_MAX); if (borderlineA || borderlineB || borderlineC) { - return TC_RETRY; // blink pattern for borderline + return TC_WARNING; // blink pattern for borderline } return TC_PASS; } - - // ===== Test case framework ===== @@ -577,7 +586,6 @@ static const TestCaseDef testCases[] = { { 5000, tc2_init, tc2_update, tc2_eval }, { 5000, tc3_init, tc3_update, tc3_eval }, { 5000, tc4_init, tc4_update, tc4_eval }, - }; static const size_t NUM_TEST_CASES = sizeof(testCases) / sizeof(testCases[0]); From d9206305839c7f3d6fabdf0862a581c5a609fcb6 Mon Sep 17 00:00:00 2001 From: Mark Benson Date: Mon, 8 Sep 2025 11:23:11 +0100 Subject: [PATCH 09/12] Add TC5 --- software/firmware/tester_runtime/src/main.c | 124 +++++++++++++++++++- 1 file changed, 120 insertions(+), 4 deletions(-) diff --git a/software/firmware/tester_runtime/src/main.c b/software/firmware/tester_runtime/src/main.c index dd8e4a6..39c84cb 100644 --- a/software/firmware/tester_runtime/src/main.c +++ b/software/firmware/tester_runtime/src/main.c @@ -578,14 +578,130 @@ static TestCaseState tc4_eval(void) { return TC_PASS; } +// ===== TC5: Unlock Pattern – State Transition / Timeout + Recovery ===== + +// Sequence constants: A=0, B=1, C=2 +static const uint8_t correctSeq[5] = { 0, 1, 2, 1, 0 }; +#define MAX_INPUTS 5 +#define MAX_ATTEMPTS 3 +#define RECOVERY_WINDOW_MS 5000 + +// State variables +static uint8_t inputSeq[MAX_INPUTS]; +static uint8_t inputCount; +static uint8_t attempts; +static bool inRecovery; +static uint32_t recoveryStart; +static bool lastWasC; +static TestCaseState tc5Outcome; + +static void tc5_init(void) { + inputCount = 0; + attempts = 0; + inRecovery = false; + recoveryStart = 0; + lastWasC = false; + tc5Outcome = TC_IN_PROGRESS; + + // Show alternating LEDs for "in progress" + tcResults[currentTest] = TC_IN_PROGRESS; + setTestCaseResult(tcResults); +} + +static void tc5_update(void) { + // Handle recovery timeout + if (inRecovery && (msTicks - recoveryStart >= RECOVERY_WINDOW_MS)) { + attempts++; + tc5Outcome = (attempts >= MAX_ATTEMPTS) ? TC_FAIL : TC_FAIL; // both solid fail + inRecovery = false; + tcResults[currentTest] = tc5Outcome; + setTestCaseResult(tcResults); + return; + } + + // Check for button presses + for (uint8_t i = 0; i < 3; i++) { + if (buttons[i].justPressed) { + buttons[i].justPressed = false; + + if (inRecovery) { + // Recovery mode: look for C -> C + if (i == 2) { // C + if (lastWasC) { + // Recovery success: reset sequence + inRecovery = false; + inputCount = 0; + lastWasC = false; + tc5Outcome = TC_IN_PROGRESS; + tcResults[currentTest] = TC_IN_PROGRESS; + setTestCaseResult(tcResults); + return; + } else { + lastWasC = true; + } + } else { + lastWasC = false; + } + return; + } + + // Normal mode + if (inputCount < MAX_INPUTS) { + inputSeq[inputCount++] = i; + + if (inputCount == MAX_INPUTS) { + // Sequence complete — check correctness + bool correct = true; + for (uint8_t j = 0; j < MAX_INPUTS; j++) { + if (inputSeq[j] != correctSeq[j]) { + correct = false; + break; + } + } + if (correct) { + tc5Outcome = TC_PASS; + tcResults[currentTest] = TC_PASS; + setTestCaseResult(tcResults); + } else { + attempts++; + if (attempts >= MAX_ATTEMPTS) { + tc5Outcome = TC_FAIL; + tcResults[currentTest] = TC_FAIL; + setTestCaseResult(tcResults); + } else { + // Enter recovery mode + inRecovery = true; + recoveryStart = msTicks; + lastWasC = false; + tc5Outcome = TC_RETRY; // fast blink for active recovery + tcResults[currentTest] = TC_RETRY; + setTestCaseResult(tcResults); + } + } + } + } + } + } +} + +static TestCaseState tc5_eval(void) { + // If still in progress when time expires, treat as fail + if (tc5Outcome == TC_IN_PROGRESS) { + return TC_FAIL; + } + return tc5Outcome; +} + + // ===== Test case framework ===== static const TestCaseDef testCases[] = { - { 5000, tc1_init, tc1_update, tc1_eval }, - { 5000, tc2_init, tc2_update, tc2_eval }, - { 5000, tc3_init, tc3_update, tc3_eval }, - { 5000, tc4_init, tc4_update, tc4_eval }, + // { 5000, tc1_init, tc1_update, tc1_eval }, + // { 5000, tc2_init, tc2_update, tc2_eval }, + // { 5000, tc3_init, tc3_update, tc3_eval }, + // { 5000, tc4_init, tc4_update, tc4_eval }, + { 10000, tc5_init, tc5_update, tc5_eval } }; static const size_t NUM_TEST_CASES = sizeof(testCases) / sizeof(testCases[0]); From 6e32a8e12105d0e3255447ca57ba89cb12af6a2e Mon Sep 17 00:00:00 2001 From: Mark Benson Date: Fri, 12 Sep 2025 12:04:41 +0100 Subject: [PATCH 10/12] Refactor code into logical modules --- .../firmware/tester_runtime/src/buttons.c | 47 ++ .../firmware/tester_runtime/src/buttons.h | 18 + .../firmware/tester_runtime/src/globals.h | 12 + .../firmware/tester_runtime/src/hardware.c | 149 ++++ .../firmware/tester_runtime/src/hardware.h | 15 + software/firmware/tester_runtime/src/leds.c | 66 ++ software/firmware/tester_runtime/src/leds.h | 19 + software/firmware/tester_runtime/src/main.c | 756 +----------------- software/firmware/tester_runtime/src/pins.h | 46 ++ .../firmware/tester_runtime/src/test_cases.c | 301 +++++++ .../firmware/tester_runtime/src/test_cases.h | 28 + 11 files changed, 718 insertions(+), 739 deletions(-) create mode 100644 software/firmware/tester_runtime/src/buttons.c create mode 100644 software/firmware/tester_runtime/src/buttons.h create mode 100644 software/firmware/tester_runtime/src/globals.h create mode 100644 software/firmware/tester_runtime/src/hardware.c create mode 100644 software/firmware/tester_runtime/src/hardware.h create mode 100644 software/firmware/tester_runtime/src/leds.c create mode 100644 software/firmware/tester_runtime/src/leds.h create mode 100644 software/firmware/tester_runtime/src/pins.h create mode 100644 software/firmware/tester_runtime/src/test_cases.c create mode 100644 software/firmware/tester_runtime/src/test_cases.h diff --git a/software/firmware/tester_runtime/src/buttons.c b/software/firmware/tester_runtime/src/buttons.c new file mode 100644 index 0000000..c764a15 --- /dev/null +++ b/software/firmware/tester_runtime/src/buttons.c @@ -0,0 +1,47 @@ + + +#include "pins.h" +#include "buttons.h" +#include +#include +#include "ch32fun.h" +#include "hardware.h" +#include "globals.h" +#include "pins.h" + +volatile ButtonState buttons[4]; +static uint8_t lastButtonSample = 0xFF; +static uint32_t initLedOffAt = 0; + +void pollButtons(void) { + uint8_t sample = 0; + for (int i = 0; i < 4; i++) { + if (!gpio_get(BTN[i].port, BTN[i].mask)) { + sample |= (1 << i); + } + } + + uint8_t changed = sample ^ lastButtonSample; + if (changed) { + for (int i = 0; i < 4; i++) { + if ((changed & (1 << i)) && (sample & (1 << i))) { + buttons[i].pressed = true; + buttons[i].justPressed = true; + buttons[i].lastPressTime = msTicks; + buttons[i].pressCount++; + gpio_clear(GPIOC, BIT(6)); + initLedOffAt = msTicks + 50; + } else if (changed & (1 << i)) { + buttons[i].pressed = false; + buttons[i].lastReleaseTime = msTicks; + } + } + } + + if (initLedOffAt && msTicks >= initLedOffAt) { + gpio_set(GPIOC, BIT(6)); + initLedOffAt = 0; + } + + lastButtonSample = sample; +} diff --git a/software/firmware/tester_runtime/src/buttons.h b/software/firmware/tester_runtime/src/buttons.h new file mode 100644 index 0000000..4fa3ca8 --- /dev/null +++ b/software/firmware/tester_runtime/src/buttons.h @@ -0,0 +1,18 @@ +#ifndef BUTTONS_H +#define BUTTONS_H + +#include +#include + +typedef struct { + uint32_t lastPressTime; + uint32_t lastReleaseTime; + uint16_t pressCount; + bool pressed; + bool justPressed; +} ButtonState; + +extern volatile ButtonState buttons[4]; +void pollButtons(void); + +#endif // BUTTONS_H diff --git a/software/firmware/tester_runtime/src/globals.h b/software/firmware/tester_runtime/src/globals.h new file mode 100644 index 0000000..14ba8db --- /dev/null +++ b/software/firmware/tester_runtime/src/globals.h @@ -0,0 +1,12 @@ +#ifndef GLOBALS_H +#define GLOBALS_H +#include +#include + +extern volatile uint32_t msTicks; +extern volatile bool flashState; +extern volatile bool rapidFlashState; +extern volatile bool runMode; +extern volatile uint8_t currentCol; + +#endif // GLOBALS_H diff --git a/software/firmware/tester_runtime/src/hardware.c b/software/firmware/tester_runtime/src/hardware.c new file mode 100644 index 0000000..a9318d9 --- /dev/null +++ b/software/firmware/tester_runtime/src/hardware.c @@ -0,0 +1,149 @@ + +#include +#include +#include "ch32fun.h" +#include "pins.h" +#include "hardware.h" +#include "buttons.h" +#include "leds.h" +#include "globals.h" +#include "pins.h" + +// Pin port/mask arrays/structs (define here) +const PinDef COL[5] = { + {GPIOC, 0x01}, {GPIOC, 0x02}, {GPIOC, 0x04}, {GPIOC, 0x08}, {GPIOC, 0x10} +}; +const PinDef ROW_RED = { GPIOA, 0x02 }; +const PinDef ROW_GREEN = { GPIOA, 0x04 }; +const PinDef BTN[4] = { + {GPIOD, 0x04}, {GPIOD, 0x08}, {GPIOD, 0x10}, {GPIOD, 0x20} +}; + +// Global variables (define here) +volatile uint32_t msTicks = 0; +volatile bool flashState = false; +volatile bool rapidFlashState = false; +volatile bool runMode = false; +volatile uint8_t currentCol = 0; + +static const int statusLEDs[] = { PIN_PWR, PIN_INIT, PIN_RDY, PIN_RUN, PIN_IDLE }; +static const int testCols[] = { PIN_COL_A, PIN_COL_B, PIN_COL_C, PIN_COL_D, PIN_COL_E }; +static const int inputPins[] = { PIN_INPUT_A, PIN_INPUT_B, PIN_INPUT_C, PIN_INPUT_D }; + +// Timer ISR for 1ms tick, button polling, and LED scan +void TIM1_UP_IRQHandler(void) INTERRUPT_DECORATOR; +void TIM1_UP_IRQHandler(void) +{ + if (TIM1->INTFR & TIM_UIF) { + TIM1->INTFR &= ~TIM_UIF; + msTicks++; + pollButtons(); + static uint32_t nextFlashAt = 250; + if (msTicks >= nextFlashAt) { + nextFlashAt += 250; + flashState = !flashState; + } + static uint32_t nextRapidFlashAt = 125; + if (msTicks >= nextRapidFlashAt) { + nextRapidFlashAt += 125; + rapidFlashState = !rapidFlashState; + } + scanStep(); + } +} + +void setupPins(void) { + funGpioInitAll(); + RCC->APB2PCENR |= RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD; + for (int i = 0; i < 5; i++) { + funPinMode(statusLEDs[i], GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); + funDigitalWrite(statusLEDs[i], FUN_HIGH); + } + for (int i = 0; i < 5; i++) { + funPinMode(testCols[i], GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); + funDigitalWrite(testCols[i], FUN_LOW); + } + funPinMode(PIN_ROW_R, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); + funPinMode(PIN_ROW_G, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); + funDigitalWrite(PIN_ROW_R, FUN_HIGH); + funDigitalWrite(PIN_ROW_G, FUN_HIGH); + for (int i = 0; i < 4; i++) { + funPinMode(inputPins[i], GPIO_CNF_IN_PUPD); + funDigitalWrite(inputPins[i], FUN_HIGH); + } +} + +void setupStatusFlasher(void) { + RCC->APB2PCENR |= RCC_APB2Periph_TIM1; + RCC->APB2PRSTR |= RCC_APB2Periph_TIM1; + RCC->APB2PRSTR &= ~RCC_APB2Periph_TIM1; + TIM1->PSC = 48 - 1; + TIM1->ATRLR = 1000 - 1; + TIM1->SWEVGR |= TIM_UG; + TIM1->INTFR &= ~TIM_UIF; + TIM1->DMAINTENR |= TIM_UIE; + NVIC_EnableIRQ(TIM1_UP_IRQn); + TIM1->CTLR1 |= TIM_CEN; +} + +void runStatus(bool isRun) { + runMode = isRun; + funDigitalWrite(PIN_RUN, FUN_HIGH); + funDigitalWrite(PIN_IDLE, FUN_HIGH); +} + +void waitTicks(uint32_t ticks) { + uint32_t start = msTicks; + while ((uint32_t)(msTicks - start) < ticks) { + __WFI(); + } +} + +void scanStep(void) { + static int8_t prev = -1; + if (activeStates == NULL) { + gpio_set(ROW_RED.port, ROW_RED.mask); + gpio_set(ROW_GREEN.port, ROW_GREEN.mask); + return; + } + gpio_set(ROW_RED.port, ROW_RED.mask); + gpio_set(ROW_GREEN.port, ROW_GREEN.mask); + if (prev >= 0 && prev < 5) { + if (COL[prev].port != NULL) { + gpio_clear(COL[prev].port, COL[prev].mask); + } + } + if (currentCol >= 0 && currentCol < 5) { + TestCaseState state = activeStates[currentCol]; + switch (state) { + case TC_PASS: + gpio_clear(ROW_GREEN.port, ROW_GREEN.mask); + break; + case TC_FAIL: + gpio_clear(ROW_RED.port, ROW_RED.mask); + break; + case TC_IN_PROGRESS: + if (flashState) + gpio_clear(ROW_RED.port, ROW_RED.mask); + else + gpio_clear(ROW_GREEN.port, ROW_GREEN.mask); + break; + case TC_WARNING: + if (flashState) + gpio_clear(ROW_RED.port, ROW_RED.mask); + break; + case TC_RETRY: + if (rapidFlashState) + gpio_clear(ROW_RED.port, ROW_RED.mask); + break; + case TC_NO_RESULT: + default: + break; + } + if (COL[currentCol].port != NULL) { + gpio_set(COL[currentCol].port, COL[currentCol].mask); + } + } + prev = currentCol; + currentCol = (currentCol + 1) % 5; +} diff --git a/software/firmware/tester_runtime/src/hardware.h b/software/firmware/tester_runtime/src/hardware.h new file mode 100644 index 0000000..1d230b8 --- /dev/null +++ b/software/firmware/tester_runtime/src/hardware.h @@ -0,0 +1,15 @@ +#ifndef HARDWARE_H +#define HARDWARE_H + +#include +#include +#include "ch32fun.h" +#include "test_cases.h" + +void setupPins(void); +void setupStatusFlasher(void); +void runStatus(bool isRun); +void waitTicks(uint32_t ticks); +void scanStep(void); + +#endif // HARDWARE_H diff --git a/software/firmware/tester_runtime/src/leds.c b/software/firmware/tester_runtime/src/leds.c new file mode 100644 index 0000000..06c5ab1 --- /dev/null +++ b/software/firmware/tester_runtime/src/leds.c @@ -0,0 +1,66 @@ + + + +#include +#include +#include "ch32fun.h" +#include "hardware.h" +#include "test_cases.h" +#include "globals.h" +#include "pins.h" + +// Double-buffered states (publish by pointer swap) +static TestCaseState bufA[5], bufB[5]; +volatile const TestCaseState* activeStates = bufA; +static uint8_t activeIdx = 0; + + + +void initTestCaseStates(void) { + for (int i = 0; i < 5; i++) { bufA[i] = bufB[i] = TC_NO_RESULT; } + activeStates = bufA; + activeIdx = 0; +} + +void setTestCaseResult(const TestCaseState states[5]) { + TestCaseState* dst = (activeIdx == 0) ? bufB : bufA; + for (int i = 0; i < 5; i++) dst[i] = states[i]; + __disable_irq(); + __asm volatile ("" ::: "memory"); + activeStates = (volatile const TestCaseState*)dst; + activeIdx ^= 1; + __asm volatile ("" ::: "memory"); + __enable_irq(); +} + +void serviceStatusLeds(void) { + static bool lastFlash = 0, lastMode = 0; + if (flashState != lastFlash || runMode != lastMode) { + uint16_t activePin = runMode ? PIN_RUN : PIN_IDLE; + uint16_t inactivePin = runMode ? PIN_IDLE : PIN_RUN; + funDigitalWrite(activePin, flashState ? FUN_LOW : FUN_HIGH); + funDigitalWrite(inactivePin, FUN_HIGH); + lastFlash = flashState; + lastMode = runMode; + } +} + +void testCaseLEDStartupPattern(void) { + static TestCaseState demoStates[5]; + for (int k = 0; k < 4; k++) { + for (int i = 0; i < 5; i++) { + for (int j = 0; j < 5; j++) demoStates[j] = TC_NO_RESULT; + demoStates[i] = TC_PASS; + setTestCaseResult(demoStates); + waitTicks(30); + } + for (int i = 4; i >= 0; i--) { + for (int j = 0; j < 5; j++) demoStates[j] = TC_NO_RESULT; + demoStates[i] = TC_FAIL; + setTestCaseResult(demoStates); + waitTicks(30); + } + } + for (int j = 0; j < 5; j++) demoStates[j] = TC_NO_RESULT; + setTestCaseResult(demoStates); +} diff --git a/software/firmware/tester_runtime/src/leds.h b/software/firmware/tester_runtime/src/leds.h new file mode 100644 index 0000000..42ad518 --- /dev/null +++ b/software/firmware/tester_runtime/src/leds.h @@ -0,0 +1,19 @@ +#include +#include "test_cases.h" + +extern volatile const TestCaseState* activeStates; +#ifndef LEDS_H +#define LEDS_H + +#include +#include + + +#include "test_cases.h" + +void serviceStatusLeds(void); +void setTestCaseResult(const TestCaseState states[5]); +void initTestCaseStates(void); +void testCaseLEDStartupPattern(void); + +#endif // LEDS_H diff --git a/software/firmware/tester_runtime/src/main.c b/software/firmware/tester_runtime/src/main.c index 39c84cb..f6c511f 100644 --- a/software/firmware/tester_runtime/src/main.c +++ b/software/firmware/tester_runtime/src/main.c @@ -1,106 +1,15 @@ #include "ch32fun.h" #include - -// -------------------- Pin definitions -------------------- - -// Status LEDs (active LOW) -#define PIN_PWR PC5 -#define PIN_INIT PC6 -#define PIN_RDY PC7 -#define PIN_RUN PD0 -#define PIN_IDLE PD6 - -// Test Case Columns (TC1–TC5), active HIGH -#define PIN_COL_A PC0 -#define PIN_COL_B PC1 -#define PIN_COL_C PC2 -#define PIN_COL_D PC3 -#define PIN_COL_E PC4 - -// Test Case Rows (active LOW) -#define PIN_ROW_R PA1 // Red (fail) -#define PIN_ROW_G PA2 // Green (pass) - -// Input buttons (assumed active LOW with pull-ups) -#define PIN_INPUT_A PD2 -#define PIN_INPUT_B PD3 -#define PIN_INPUT_C PD4 -#define PIN_INPUT_D PD5 - -// -------------------- Timing -------------------- - -#define TICKS_PER_SECOND 1000 // 1 ms tick - -// -------------------- Tables -------------------- - -static const uint16_t statusLEDs[] = { PIN_PWR, PIN_INIT, PIN_RDY, PIN_RUN, PIN_IDLE }; -static const uint16_t testCols[] = { PIN_COL_A, PIN_COL_B, PIN_COL_C, PIN_COL_D, PIN_COL_E }; -static const uint16_t testRows[] = { PIN_ROW_R, PIN_ROW_G }; -static const uint16_t inputPins[] = { PIN_INPUT_A, PIN_INPUT_B, PIN_INPUT_C, PIN_INPUT_D }; - -// -------------------- State and flags -------------------- - -typedef struct { - uint32_t lastPressTime; - uint32_t lastReleaseTime; - uint16_t pressCount; - bool pressed; // true while button is physically held down - bool justPressed; // true only on the tick a press is first detected, until consumed -} ButtonState; - -volatile ButtonState buttons[4]; - -static uint8_t lastButtonSample = 0xFF; // all released (active-low) - - -typedef enum { - TC_NO_RESULT, - TC_PASS, - TC_FAIL, - TC_WARNING, - TC_IN_PROGRESS, - TC_RETRY -} TestCaseState; - -volatile bool runMode = false; // false = IDLE, true = RUN -volatile bool flashState = false; // toggles every 250 ms -volatile bool rapidFlashState = false; // toggles every 125 ms -volatile uint32_t msTicks = 0; // monotonic ms counter -volatile uint8_t currentCol = 0; // scan index - -// Double-buffered states (publish by pointer swap) -static TestCaseState bufA[5], bufB[5]; -static volatile const TestCaseState* activeStates = bufA; -static uint8_t activeIdx = 0; +#include "globals.h" +#include "pins.h" +#include "hardware.h" +#include "leds.h" +#include "buttons.h" +#include "test_cases.h" // Optional: trap registers for debug (RISC-V) volatile uint32_t last_mcause = 0, last_mepc = 0; - -// --- Direct GPIO helpers --- -#define BIT(n) (1U << (n)) -static inline void gpio_set(GPIO_TypeDef* p, uint16_t m) { p->BSHR = m; } -static inline void gpio_clear(GPIO_TypeDef* p, uint16_t m) { p->BCR = m; } -static inline uint8_t gpio_get(GPIO_TypeDef* p, uint16_t m) { - return (p->INDR & m) ? 1U : 0U; -} - -// --- Explicit port/mask map for columns and rows --- -static const struct { GPIO_TypeDef* port; uint16_t mask; } COL[5] = { - {GPIOC, BIT(0)}, {GPIOC, BIT(1)}, {GPIOC, BIT(2)}, {GPIOC, BIT(3)}, {GPIOC, BIT(4)} -}; - -static const struct { GPIO_TypeDef* port; uint16_t mask; } ROW_RED = { GPIOA, BIT(1) }; -static const struct { GPIO_TypeDef* port; uint16_t mask; } ROW_GREEN = { GPIOA, BIT(2) }; - -// port/mask map for input buttons -static const struct { GPIO_TypeDef* port; uint16_t mask; } BTN[4] = { - {GPIOD, BIT(2)}, {GPIOD, BIT(3)}, {GPIOD, BIT(4)}, {GPIOD, BIT(5)} -}; - - -// -------------------- Hard fault handler -------------------- - void HardFault_Handler(void) { __disable_irq(); @@ -109,666 +18,35 @@ void HardFault_Handler(void) for (;;) { /* spin */ } } -// -------------------- Helpers -------------------- - -static inline void initTestCaseStates(void) { - for (int i = 0; i < 5; i++) { bufA[i] = bufB[i] = TC_NO_RESULT; } - activeStates = bufA; - activeIdx = 0; -} - -static inline void setTestCaseResult(const TestCaseState states[5]) { - // Copy into the inactive buffer with IRQs enabled - TestCaseState* dst = (activeIdx == 0) ? bufB : bufA; - for (int i = 0; i < 5; i++) dst[i] = states[i]; - - // Publish with a tiny critical section (pointer swap) + compiler barriers - __disable_irq(); - __asm volatile ("" ::: "memory"); - activeStates = (volatile const TestCaseState*)dst; - activeIdx ^= 1; - __asm volatile ("" ::: "memory"); - __enable_irq(); -} - -static inline void scanStep(void) { - static int8_t prev = -1; // last active column, -1 = none yet - - // 0. Bail out early if state pointer is invalid - volatile const TestCaseState* states = activeStates; - if (states == NULL) { - // Ensure all LEDs off - gpio_set(ROW_RED.port, ROW_RED.mask); - gpio_set(ROW_GREEN.port, ROW_GREEN.mask); - return; - } - - // 1. Turn both rows OFF (active LOW → drive HIGH) before touching columns - gpio_set(ROW_RED.port, ROW_RED.mask); - gpio_set(ROW_GREEN.port, ROW_GREEN.mask); - - // 2. Clear the previous column if it was valid - if (prev >= 0 && prev < 5) { - if (COL[prev].port != NULL) { - gpio_clear(COL[prev].port, COL[prev].mask); - } - } - - // 3. Decide what to light for the current column (only if in range) - if (currentCol >= 0 && currentCol < 5) { - TestCaseState state = states[currentCol]; - - switch (state) { - case TC_PASS: - gpio_clear(ROW_GREEN.port, ROW_GREEN.mask); // green ON - break; - case TC_FAIL: - gpio_clear(ROW_RED.port, ROW_RED.mask); // red ON - break; - case TC_IN_PROGRESS: - if (flashState) - gpio_clear(ROW_RED.port, ROW_RED.mask); - else - gpio_clear(ROW_GREEN.port, ROW_GREEN.mask); - break; - case TC_WARNING: - if (flashState) - gpio_clear(ROW_RED.port, ROW_RED.mask); - break; - case TC_RETRY: - if (rapidFlashState) - gpio_clear(ROW_RED.port, ROW_RED.mask); - break; - case TC_NO_RESULT: - default: - // both rows stay off - break; - } - - // 4. Set the current column HIGH (active HIGH) - if (COL[currentCol].port != NULL) { - gpio_set(COL[currentCol].port, COL[currentCol].mask); - } - } - - // 5. Advance indices for next tick - prev = currentCol; - currentCol = (currentCol + 1) % 5; -} - - -static inline void waitTicks(uint32_t ticks) { - uint32_t start = msTicks; - while ((uint32_t)(msTicks - start) < ticks) { - __WFI(); - } -} - -static inline void serviceStatusLeds(void) { - // Update RUN/IDLE (active-low) outside ISR - static bool lastFlash = 0, lastMode = 0; - if (flashState != lastFlash || runMode != lastMode) { - uint16_t activePin = runMode ? PIN_RUN : PIN_IDLE; - uint16_t inactivePin = runMode ? PIN_IDLE : PIN_RUN; - funDigitalWrite(activePin, flashState ? FUN_LOW : FUN_HIGH); - funDigitalWrite(inactivePin, FUN_HIGH); - lastFlash = flashState; - lastMode = runMode; - } -} - -static uint32_t initLedOffAt = 0; - -static inline void pollButtons(void) { - uint8_t sample = 0; - for (int i = 0; i < 4; i++) { - if (!gpio_get(BTN[i].port, BTN[i].mask)) { - sample |= (1 << i); - } - } - - uint8_t changed = sample ^ lastButtonSample; - if (changed) { - for (int i = 0; i < 4; i++) { - if ((changed & (1 << i)) && (sample & (1 << i))) { - // Rising edge: Pressed - buttons[i].pressed = true; // level - buttons[i].justPressed = true; // new: one-shot event - buttons[i].lastPressTime = msTicks; - buttons[i].pressCount++; - - // Start INIT LED pulse - gpio_clear(GPIOC, BIT(6)); // INIT LED on (active low) - initLedOffAt = msTicks + 50; // 50 ms pulse - - } else if (changed & (1 << i)) { - // Falling edge: Released - buttons[i].pressed = false; - buttons[i].lastReleaseTime = msTicks; - } - } - } - - // End INIT LED pulse if time elapsed - if (initLedOffAt && msTicks >= initLedOffAt) { - gpio_set(GPIOC, BIT(6)); // INIT LED off - initLedOffAt = 0; - } - - lastButtonSample = sample; -} - -// -------------------- Timer ISR -------------------- - -void TIM1_UP_IRQHandler(void) INTERRUPT_DECORATOR; -void TIM1_UP_IRQHandler(void) -{ - if (TIM1->INTFR & TIM_UIF) { - TIM1->INTFR &= ~TIM_UIF; - msTicks++; - pollButtons(); - static uint32_t nextFlashAt = 250; - if (msTicks >= nextFlashAt) { - nextFlashAt += 250; - flashState = !flashState; - } - static uint32_t nextRapidFlashAt = 125; - if (msTicks >= nextRapidFlashAt) { - nextRapidFlashAt += 125; - rapidFlashState = !rapidFlashState; - } - scanStep(); - - } -} - -// -------------------- Hardware setup -------------------- - -static inline void setupStatusFlasher(void) -{ - // Enable and reset TIM1 (APB2) - RCC->APB2PCENR |= RCC_APB2Periph_TIM1; - RCC->APB2PRSTR |= RCC_APB2Periph_TIM1; - RCC->APB2PRSTR &= ~RCC_APB2Periph_TIM1; - - // 48 MHz -> 1 MHz timer, ARR=1000-1 => 1 ms - TIM1->PSC = 48 - 1; - TIM1->ATRLR = 1000 - 1; - TIM1->SWEVGR |= TIM_UG; - TIM1->INTFR &= ~TIM_UIF; - TIM1->DMAINTENR |= TIM_UIE; - - NVIC_EnableIRQ(TIM1_UP_IRQn); - TIM1->CTLR1 |= TIM_CEN; -} - -static inline void runStatus(bool isRun) -{ - runMode = isRun; - // ensure both OFF, main loop will handle blinking - funDigitalWrite(PIN_RUN, FUN_HIGH); - funDigitalWrite(PIN_IDLE, FUN_HIGH); -} - -static inline void setupPins(void) { - funGpioInitAll(); - - // Explicit clocks for ports we touch directly - RCC->APB2PCENR |= RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD; - - // Status LEDs (active LOW) - for (int i = 0; i < 5; i++) { - funPinMode(statusLEDs[i], GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); - funDigitalWrite(statusLEDs[i], FUN_HIGH); // OFF - } - - // Columns configured via ch32fun (active HIGH, start OFF) - for (int i = 0; i < 5; i++) { - funPinMode(testCols[i], GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); - funDigitalWrite(testCols[i], FUN_LOW); - } - - // Rows (active LOW → OFF = HIGH) - funPinMode(PIN_ROW_R, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); - funPinMode(PIN_ROW_G, GPIO_Speed_10MHz | GPIO_CNF_OUT_PP); - funDigitalWrite(PIN_ROW_R, FUN_HIGH); - funDigitalWrite(PIN_ROW_G, FUN_HIGH); - - // Inputs with pull-ups - for (int i = 0; i < 4; i++) { - funPinMode(inputPins[i], GPIO_CNF_IN_PUPD); - funDigitalWrite(inputPins[i], FUN_HIGH); - } -} - -static inline void testCaseLEDStartupPattern(void) { - static TestCaseState demoStates[5]; - - // Repeat the above 3 times so it looks like a "chase" - for (int k = 0; k < 4; k++) { - for (int i = 0; i < 5; i++) { - for (int j = 0; j < 5; j++) demoStates[j] = TC_NO_RESULT; - demoStates[i] = TC_PASS; - setTestCaseResult(demoStates); - waitTicks(30); - } - for (int i = 4; i >= 0; i--) { - for (int j = 0; j < 5; j++) demoStates[j] = TC_NO_RESULT; - demoStates[i] = TC_FAIL; - setTestCaseResult(demoStates); - waitTicks(30); - } - } - // end with all off - for (int j = 0; j < 5; j++) demoStates[j] = TC_NO_RESULT; - setTestCaseResult(demoStates); -} - -static inline void startupSequence(void) { +static void startupSequence(void) { + // Power LED on, blink INIT, RDY on, start timer, set IDLE flashing, LED pattern funDigitalWrite(PIN_PWR, FUN_LOW); // PWR stays on - - // Blink INIT 3 times for (int i = 0; i < 3; i++) { funDigitalWrite(PIN_INIT, FUN_LOW); Delay_Ms(200); funDigitalWrite(PIN_INIT, FUN_HIGH); Delay_Ms(200); } - - // RDY on funDigitalWrite(PIN_RDY, FUN_LOW); - - // Start timer and set IDLE flashing setupStatusFlasher(); - __enable_irq(); // enable global interrupts + __enable_irq(); runStatus(false); testCaseLEDStartupPattern(); } -static inline void runTestCaseDemo(void) { - static TestCaseState demoStates[5]; - - // Pattern 1 - demoStates[0] = TC_PASS; - demoStates[1] = TC_FAIL; - demoStates[2] = TC_IN_PROGRESS; - demoStates[3] = TC_PASS; - demoStates[4] = TC_RETRY; - setTestCaseResult(demoStates); - waitTicks(5 * TICKS_PER_SECOND); - - // Pattern 2 - demoStates[0] = TC_FAIL; - demoStates[1] = TC_PASS; - demoStates[2] = TC_FAIL; - demoStates[3] = TC_RETRY; - demoStates[4] = TC_IN_PROGRESS; - setTestCaseResult(demoStates); - waitTicks(5 * TICKS_PER_SECOND); - - // Pattern 3 (all pass) - for (int i = 0; i < 5; i++) demoStates[i] = TC_PASS; - setTestCaseResult(demoStates); - waitTicks(5 * TICKS_PER_SECOND); - - // Pattern 4 (all fail) - for (int i = 0; i < 5; i++) demoStates[i] = TC_FAIL; - setTestCaseResult(demoStates); -} - -// -------------------- App logic -------------------- - -static TestCaseState tcResults[5] = { TC_NO_RESULT }; -typedef struct { - uint32_t durationMs; - void (*initFn)(void); - void (*updateFn)(void); - TestCaseState (*evalFn)(void); -} TestCaseDef; - - -static uint8_t currentTest = 0; -static uint32_t testStartTime = 0; -static bool testActive = false; - -// ===== TC1: All A–C pressed ===== -static bool seenA, seenB, seenC; - -static void tc1_init(void) { - seenA = seenB = seenC = false; -} - -static void tc1_update(void) { - if (buttons[0].pressed) seenA = true; - if (buttons[1].pressed) seenB = true; - if (buttons[2].pressed) seenC = true; -} - -static TestCaseState tc1_eval(void) { - return (seenA && seenB && seenC) ? TC_PASS : TC_FAIL; -} - -// ===== TC2: Debounce timing ===== -static uint32_t firstPressTimeB, lastPressTimeB; -static uint8_t pressCountB; -static const uint32_t DEBOUNCE_THRESHOLD_MS = 200; - -// TC2: Fast double-press detection -static uint32_t pressTimes[4]; -static uint8_t pressIndex; -static const uint32_t FAST_THRESHOLD_MS = 200; -static const uint32_t MIN_GAP_MS = 30; // reject bounce - -static void tc2_init(void) { - pressIndex = 0; -} - -static void tc2_update(void) { - if (buttons[1].justPressed) { - buttons[1].justPressed = false; // consume event - if (pressIndex < 4) { - pressTimes[pressIndex++] = msTicks; - } - } -} - -static TestCaseState tc2_eval(void) { - if (pressCountB < 2) { - return TC_FAIL; // no presses or only one press - } - - uint32_t gap = lastPressTimeB - firstPressTimeB; - - if (gap < MIN_GAP_MS) { - return TC_FAIL; // bounce - } - if (gap >= FAST_THRESHOLD_MS) { - return TC_FAIL; // too slow - } - - return TC_PASS; // valid fast double press -} - - -// ===== TC3: Conditional Logic – Decision Table Test ===== - -// Latch states for A, B, C during the 5s window -static bool seenA3, seenB3, seenC3; - -static void tc3_init(void) { - seenA3 = seenB3 = seenC3 = false; -} - -static void tc3_update(void) { - if (buttons[0].justPressed) { - buttons[0].justPressed = false; - seenA3 = !seenA3; // toggle - } - if (buttons[1].justPressed) { - buttons[1].justPressed = false; - seenB3 = !seenB3; // toggle - } - if (buttons[2].justPressed) { - buttons[2].justPressed = false; - seenC3 = !seenC3; // toggle - } -} - - -// Decision table returning your existing TestCaseState -static TestCaseState tc3_eval(void) { - // Decision table mapping - if (!seenA3 && !seenB3 && !seenC3) return TC_FAIL; // 000 - if (!seenA3 && !seenB3 && seenC3) return TC_WARNING; // 001 - if (!seenA3 && seenB3 && !seenC3) return TC_WARNING; // 010 - if (!seenA3 && seenB3 && seenC3) return TC_PASS; // 011 ✅ - if ( seenA3 && !seenB3 && !seenC3) return TC_FAIL; // 100 - if ( seenA3 && !seenB3 && seenC3) return TC_WARNING; // 101 - if ( seenA3 && seenB3 && !seenC3) return TC_WARNING; // 110 - if ( seenA3 && seenB3 && seenC3) return TC_WARNING; // 111 - return TC_FAIL; -} - - -// ===== TC4: Input Range Check ===== - -// Press counters for A, B, C -static uint8_t countA4, countB4, countC4; - -// Valid ranges -#define A_MIN 1 -#define A_MAX 3 -#define B_MIN 5 -#define B_MAX 7 -#define C_MIN 2 -#define C_MAX 4 - -static void tc4_init(void) { - countA4 = countB4 = countC4 = 0; -} - -static void tc4_update(void) { - if (buttons[0].justPressed) { buttons[0].justPressed = false; countA4++; } - if (buttons[1].justPressed) { buttons[1].justPressed = false; countB4++; } - if (buttons[2].justPressed) { buttons[2].justPressed = false; countC4++; } -} - -static TestCaseState tc4_eval(void) { - bool validA = (countA4 >= A_MIN && countA4 <= A_MAX); - bool validB = (countB4 >= B_MIN && countB4 <= B_MAX); - bool validC = (countC4 >= C_MIN && countC4 <= C_MAX); - - bool anyValid = validA || validB || validC; - - // For now, map to overall PASS/FAIL/RETRY - // You could extend this to show per‑input LEDs if hardware supports it - if (!anyValid) { - return TC_FAIL; - } - - // Optional: treat borderline values (exactly at min or max) as RETRY - bool borderlineA = validA && (countA4 == A_MIN || countA4 == A_MAX); - bool borderlineB = validB && (countB4 == B_MIN || countB4 == B_MAX); - bool borderlineC = validC && (countC4 == C_MIN || countC4 == C_MAX); - - if (borderlineA || borderlineB || borderlineC) { - return TC_WARNING; // blink pattern for borderline - } - - return TC_PASS; -} - -// ===== TC5: Unlock Pattern – State Transition / Timeout + Recovery ===== - -// Sequence constants: A=0, B=1, C=2 -static const uint8_t correctSeq[5] = { 0, 1, 2, 1, 0 }; -#define MAX_INPUTS 5 -#define MAX_ATTEMPTS 3 -#define RECOVERY_WINDOW_MS 5000 - -// State variables -static uint8_t inputSeq[MAX_INPUTS]; -static uint8_t inputCount; -static uint8_t attempts; -static bool inRecovery; -static uint32_t recoveryStart; -static bool lastWasC; -static TestCaseState tc5Outcome; - -static void tc5_init(void) { - inputCount = 0; - attempts = 0; - inRecovery = false; - recoveryStart = 0; - lastWasC = false; - tc5Outcome = TC_IN_PROGRESS; - - // Show alternating LEDs for "in progress" - tcResults[currentTest] = TC_IN_PROGRESS; - setTestCaseResult(tcResults); -} - -static void tc5_update(void) { - // Handle recovery timeout - if (inRecovery && (msTicks - recoveryStart >= RECOVERY_WINDOW_MS)) { - attempts++; - tc5Outcome = (attempts >= MAX_ATTEMPTS) ? TC_FAIL : TC_FAIL; // both solid fail - inRecovery = false; - tcResults[currentTest] = tc5Outcome; - setTestCaseResult(tcResults); - return; - } - - // Check for button presses - for (uint8_t i = 0; i < 3; i++) { - if (buttons[i].justPressed) { - buttons[i].justPressed = false; - - if (inRecovery) { - // Recovery mode: look for C -> C - if (i == 2) { // C - if (lastWasC) { - // Recovery success: reset sequence - inRecovery = false; - inputCount = 0; - lastWasC = false; - tc5Outcome = TC_IN_PROGRESS; - tcResults[currentTest] = TC_IN_PROGRESS; - setTestCaseResult(tcResults); - return; - } else { - lastWasC = true; - } - } else { - lastWasC = false; - } - return; - } - - // Normal mode - if (inputCount < MAX_INPUTS) { - inputSeq[inputCount++] = i; - - if (inputCount == MAX_INPUTS) { - // Sequence complete — check correctness - bool correct = true; - for (uint8_t j = 0; j < MAX_INPUTS; j++) { - if (inputSeq[j] != correctSeq[j]) { - correct = false; - break; - } - } - if (correct) { - tc5Outcome = TC_PASS; - tcResults[currentTest] = TC_PASS; - setTestCaseResult(tcResults); - } else { - attempts++; - if (attempts >= MAX_ATTEMPTS) { - tc5Outcome = TC_FAIL; - tcResults[currentTest] = TC_FAIL; - setTestCaseResult(tcResults); - } else { - // Enter recovery mode - inRecovery = true; - recoveryStart = msTicks; - lastWasC = false; - tc5Outcome = TC_RETRY; // fast blink for active recovery - tcResults[currentTest] = TC_RETRY; - setTestCaseResult(tcResults); - } - } - } - } - } - } -} - -static TestCaseState tc5_eval(void) { - // If still in progress when time expires, treat as fail - if (tc5Outcome == TC_IN_PROGRESS) { - return TC_FAIL; - } - return tc5Outcome; -} - - -// ===== Test case framework ===== - - -static const TestCaseDef testCases[] = { - // { 5000, tc1_init, tc1_update, tc1_eval }, - // { 5000, tc2_init, tc2_update, tc2_eval }, - // { 5000, tc3_init, tc3_update, tc3_eval }, - // { 5000, tc4_init, tc4_update, tc4_eval }, - { 10000, tc5_init, tc5_update, tc5_eval } -}; - -static const size_t NUM_TEST_CASES = sizeof(testCases) / sizeof(testCases[0]); - -static void startTest(uint8_t idx) { - if (idx == 0) { - for (size_t i = 0; i < NUM_TEST_CASES; i++) { - tcResults[i] = TC_NO_RESULT; - } - } - testStartTime = msTicks; - testActive = true; - - runStatus(true); - tcResults[idx] = TC_IN_PROGRESS; - setTestCaseResult(tcResults); - - testCases[idx].initFn(); // per-test init -} - -static void endTest(void) { - testActive = false; - runStatus(false); - - TestCaseState outcome = testCases[currentTest].evalFn(); - tcResults[currentTest] = outcome; - setTestCaseResult(tcResults); - - currentTest = (currentTest + 1) % NUM_TEST_CASES; -} - -static void monitorInputs(void) { - if (!testActive) return; - testCases[currentTest].updateFn(); // per-test input handling - if ((uint32_t)(msTicks - testStartTime) >= testCases[currentTest].durationMs) { - endTest(); - } -} - -// -------------------- Main -------------------- - -int main(void) { + int main(void) { SystemInit(); - setupPins(); - - initTestCaseStates(); // publish valid buffer before timer starts - - startupSequence(); // starts TIM1 and sets IDLE mode - - waitTicks(100); // let it settle - - // runTestCaseDemo(); - // testCaseLEDStartupPattern(); - + initTestCaseStates(); + startupSequence(); + waitTicks(100); while (1) { - serviceStatusLeds(); // update RUN/IDLE outside ISR - // START TEST button = BTN D (index 3) - + serviceStatusLeds(); if (!testActive && buttons[3].pressed) { - startTest(currentTest); + test_cases_start(currentTest); } - - monitorInputs(); - + test_cases_monitor_inputs(); __WFI(); - } - + } } diff --git a/software/firmware/tester_runtime/src/pins.h b/software/firmware/tester_runtime/src/pins.h new file mode 100644 index 0000000..b6e32dd --- /dev/null +++ b/software/firmware/tester_runtime/src/pins.h @@ -0,0 +1,46 @@ +#ifndef BIT +#define BIT(x) (1U << (x)) +#endif +#ifndef PINS_H +#define PINS_H +#include "ch32fun.h" + +// Status LEDs (active LOW) +#define PIN_PWR PC5 +#define PIN_INIT PC6 +#define PIN_RDY PC7 +#define PIN_RUN PD0 +#define PIN_IDLE PD6 + +// Test Case Columns (TC1–TC5), active HIGH +#define PIN_COL_A PC0 +#define PIN_COL_B PC1 +#define PIN_COL_C PC2 +#define PIN_COL_D PC3 +#define PIN_COL_E PC4 + +// Test Case Rows (active LOW) +#define PIN_ROW_R PA1 // Red (fail) +#define PIN_ROW_G PA2 // Green (pass) + +// Input buttons (assumed active LOW with pull-ups) +#define PIN_INPUT_A PD2 +#define PIN_INPUT_B PD3 +#define PIN_INPUT_C PD4 +#define PIN_INPUT_D PD5 + + +typedef struct { GPIO_TypeDef* port; int mask; } PinDef; + +extern const PinDef COL[5]; +extern const PinDef ROW_RED; +extern const PinDef ROW_GREEN; +extern const PinDef BTN[4]; + +static inline void gpio_set(GPIO_TypeDef* p, int m) { p->BSHR = m; } +static inline void gpio_clear(GPIO_TypeDef* p, int m) { p->BCR = m; } +static inline uint8_t gpio_get(GPIO_TypeDef* p, int m) { + return (p->INDR & m) ? 1U : 0U; +} + +#endif // PINS_H diff --git a/software/firmware/tester_runtime/src/test_cases.c b/software/firmware/tester_runtime/src/test_cases.c new file mode 100644 index 0000000..5781287 --- /dev/null +++ b/software/firmware/tester_runtime/src/test_cases.c @@ -0,0 +1,301 @@ + +#include "test_cases.h" +#include "buttons.h" +#include "leds.h" +#include "hardware.h" +#include "globals.h" +#include "pins.h" +#include +#include +#include + +TestCaseState tcResults[5] = { TC_NO_RESULT }; +uint8_t currentTest = 0; +bool testActive = false; +static uint32_t testStartTime = 0; + + +// ===== TC1: All A–C pressed ===== +static bool seenA, seenB, seenC; + +static void tc1_init(void) { + seenA = seenB = seenC = false; +} + +static void tc1_update(void) { + if (buttons[0].pressed) seenA = true; + if (buttons[1].pressed) seenB = true; + if (buttons[2].pressed) seenC = true; +} + +static TestCaseState tc1_eval(void) { + return (seenA && seenB && seenC) ? TC_PASS : TC_FAIL; +} + +// ===== TC2: Debounce timing ===== +// TC2: Fast double-press detection +static uint32_t pressTimes[4]; +static uint8_t pressIndex; +static const uint32_t FAST_THRESHOLD_MS = 200; +static const uint32_t MIN_GAP_MS = 30; // reject bounce + +static void tc2_init(void) { + pressIndex = 0; + for (uint8_t i = 0; i < 4; i++) { + pressTimes[i] = 0; + } +} + +static void tc2_update(void) { + if (buttons[1].justPressed) { + buttons[1].justPressed = false; // consume event + if (pressIndex < 4) { + pressTimes[pressIndex++] = msTicks; + } + } +} + +static TestCaseState tc2_eval(void) { + for (uint8_t i = 0; i < pressIndex - 1; i++) { + uint32_t gap = pressTimes[i + 1] - pressTimes[i]; + + if (gap >= MIN_GAP_MS && gap < FAST_THRESHOLD_MS) { + return TC_PASS; // found a valid fast double press + } + } + return TC_FAIL; // no valid pair found +} + +// ===== TC3: Conditional Logic – Decision Table Test ===== + +// Latch states for A, B, C during the 5s window +static bool seenA3, seenB3, seenC3; + +static void tc3_init(void) { + seenA3 = seenB3 = seenC3 = false; +} + +static void tc3_update(void) { + if (buttons[0].justPressed) { + buttons[0].justPressed = false; + seenA3 = !seenA3; // toggle + } + if (buttons[1].justPressed) { + buttons[1].justPressed = false; + seenB3 = !seenB3; // toggle + } + if (buttons[2].justPressed) { + buttons[2].justPressed = false; + seenC3 = !seenC3; // toggle + } +} + +// Decision table returning your existing TestCaseState +static TestCaseState tc3_eval(void) { + // Decision table mapping + if (!seenA3 && !seenB3 && !seenC3) return TC_FAIL; // 000 + if (!seenA3 && !seenB3 && seenC3) return TC_WARNING; // 001 + if (!seenA3 && seenB3 && !seenC3) return TC_WARNING; // 010 + if (!seenA3 && seenB3 && seenC3) return TC_PASS; // 011 ✅ + if ( seenA3 && !seenB3 && !seenC3) return TC_FAIL; // 100 + if ( seenA3 && !seenB3 && seenC3) return TC_WARNING; // 101 + if ( seenA3 && seenB3 && !seenC3) return TC_WARNING; // 110 + if ( seenA3 && seenB3 && seenC3) return TC_WARNING; // 111 + return TC_FAIL; +} + +// ===== TC4: Input Range Check ===== + +// Press counters for A, B, C +static uint8_t countA4, countB4, countC4; + +// Valid ranges +#define A_MIN 5 +#define A_MAX 7 +// 6 is a pass +#define B_MIN 7 +#define B_MAX 12 +// 8 - 11 are passes +#define C_MIN 2 +#define C_MAX 4 +// 3 is a pass + +static void tc4_init(void) { + countA4 = countB4 = countC4 = 0; +} + +static void tc4_update(void) { + if (buttons[0].justPressed) { buttons[0].justPressed = false; countA4++; } + if (buttons[1].justPressed) { buttons[1].justPressed = false; countB4++; } + if (buttons[2].justPressed) { buttons[2].justPressed = false; countC4++; } +} + +static TestCaseState tc4_eval(void) { + bool validA = (countA4 >= A_MIN && countA4 <= A_MAX); + bool validB = (countB4 >= B_MIN && countB4 <= B_MAX); + bool validC = (countC4 >= C_MIN && countC4 <= C_MAX); + + bool anyValid = validA || validB || validC; + + if (!anyValid) { + return TC_FAIL; + } + + // Optional: treat borderline values (exactly at min or max) as RETRY + bool borderlineA = validA && (countA4 == A_MIN || countA4 == A_MAX); + bool borderlineB = validB && (countB4 == B_MIN || countB4 == B_MAX); + bool borderlineC = validC && (countC4 == C_MIN || countC4 == C_MAX); + + if (borderlineA || borderlineB || borderlineC) { + return TC_WARNING; // blink pattern for borderline + } + + return TC_PASS; +} + +// ===== TC5: Unlock Pattern – State Transition / Timeout + Recovery ===== +static const uint8_t correctSeq[5] = { 0, 1, 2, 1, 0 }; +#define MAX_INPUTS 5 +#define MAX_ATTEMPTS 3 +#define RECOVERY_WINDOW_MS 5000 +static uint8_t inputSeq[MAX_INPUTS]; +static uint8_t inputCount; +static uint8_t attempts; +static bool inRecovery; +static uint32_t recoveryStart; +static bool lastWasC; +static TestCaseState tc5Outcome; + +static void tc5_init(void) { + inputCount = 0; + attempts = 0; + inRecovery = false; + recoveryStart = 0; + lastWasC = false; + tc5Outcome = TC_IN_PROGRESS; + tcResults[currentTest] = TC_IN_PROGRESS; + setTestCaseResult(tcResults); +} + +static void tc5_update(void) { + if (inRecovery && (msTicks - recoveryStart >= RECOVERY_WINDOW_MS)) { + attempts++; + tc5Outcome = (attempts >= MAX_ATTEMPTS) ? TC_FAIL : TC_FAIL; + inRecovery = false; + tcResults[currentTest] = tc5Outcome; + setTestCaseResult(tcResults); + return; + } + for (uint8_t i = 0; i < 3; i++) { + if (buttons[i].justPressed) { + buttons[i].justPressed = false; + if (inRecovery) { + if (i == 2) { + if (lastWasC) { + inRecovery = false; + inputCount = 0; + lastWasC = false; + tc5Outcome = TC_IN_PROGRESS; + tcResults[currentTest] = TC_IN_PROGRESS; + setTestCaseResult(tcResults); + return; + } else { + lastWasC = true; + } + } else { + lastWasC = false; + } + return; + } + if (inputCount < MAX_INPUTS) { + inputSeq[inputCount++] = i; + if (inputCount == MAX_INPUTS) { + bool correct = true; + for (uint8_t j = 0; j < MAX_INPUTS; j++) { + if (inputSeq[j] != correctSeq[j]) { + correct = false; + break; + } + } + if (correct) { + tc5Outcome = TC_PASS; + tcResults[currentTest] = TC_PASS; + setTestCaseResult(tcResults); + } else { + attempts++; + if (attempts >= MAX_ATTEMPTS) { + tc5Outcome = TC_FAIL; + tcResults[currentTest] = TC_FAIL; + setTestCaseResult(tcResults); + } else { + inRecovery = true; + recoveryStart = msTicks; + lastWasC = false; + tc5Outcome = TC_RETRY; + tcResults[currentTest] = TC_RETRY; + setTestCaseResult(tcResults); + } + } + } + } + } + } +} + +static TestCaseState tc5_eval(void) { + if (tc5Outcome == TC_IN_PROGRESS) { + return TC_FAIL; + } + return tc5Outcome; +} + +typedef struct { + uint32_t durationMs; + void (*initFn)(void); + void (*updateFn)(void); + TestCaseState (*evalFn)(void); +} TestCaseDef; + +static const TestCaseDef testCases[] = { + { 5000, tc1_init, tc1_update, tc1_eval }, + { 5000, tc2_init, tc2_update, tc2_eval }, + { 5000, tc3_init, tc3_update, tc3_eval }, + { 5000, tc4_init, tc4_update, tc4_eval }, + // { 10000, tc5_init, tc5_update, tc5_eval } +}; +static const size_t NUM_TEST_CASES = sizeof(testCases) / sizeof(testCases[0]); + +void test_cases_start(uint8_t idx) { + if (idx == 0) { + for (size_t i = 0; i < NUM_TEST_CASES; i++) { + tcResults[i] = TC_NO_RESULT; + } + } + testStartTime = msTicks; + testActive = true; + runStatus(true); + tcResults[idx] = TC_IN_PROGRESS; + setTestCaseResult(tcResults); + testCases[idx].initFn(); +} + +void test_cases_end(void) { + testActive = false; + runStatus(false); + TestCaseState outcome = testCases[currentTest].evalFn(); + tcResults[currentTest] = outcome; + setTestCaseResult(tcResults); + currentTest = (currentTest + 1) % NUM_TEST_CASES; +} + +void test_cases_monitor_inputs(void) { + if (!testActive) return; + testCases[currentTest].updateFn(); + if ((uint32_t)(msTicks - testStartTime) >= testCases[currentTest].durationMs) { + test_cases_end(); + } +} + +void test_cases_init(void) {} +void test_cases_update(void) {} +TestCaseState test_cases_eval(void) { return TC_NO_RESULT; } diff --git a/software/firmware/tester_runtime/src/test_cases.h b/software/firmware/tester_runtime/src/test_cases.h new file mode 100644 index 0000000..cc2cace --- /dev/null +++ b/software/firmware/tester_runtime/src/test_cases.h @@ -0,0 +1,28 @@ +#ifndef TEST_CASES_H +#define TEST_CASES_H + +#include +#include + +typedef enum { + TC_NO_RESULT, + TC_PASS, + TC_FAIL, + TC_WARNING, + TC_IN_PROGRESS, + TC_RETRY +} TestCaseState; + + +void test_cases_init(void); +void test_cases_update(void); +TestCaseState test_cases_eval(void); +void test_cases_start(uint8_t idx); +void test_cases_end(void); +void test_cases_monitor_inputs(void); + +extern TestCaseState tcResults[5]; +extern uint8_t currentTest; +extern bool testActive; + +#endif // TEST_CASES_H From 00bccf7853bed8a23fd93891c58b565167f80437 Mon Sep 17 00:00:00 2001 From: Mark Benson Date: Fri, 12 Sep 2025 17:00:40 +0100 Subject: [PATCH 11/12] TC5 working --- .../firmware/tester_runtime/src/test_cases.c | 173 ++++++++++-------- 1 file changed, 98 insertions(+), 75 deletions(-) diff --git a/software/firmware/tester_runtime/src/test_cases.c b/software/firmware/tester_runtime/src/test_cases.c index 5781287..1bab8a9 100644 --- a/software/firmware/tester_runtime/src/test_cases.c +++ b/software/firmware/tester_runtime/src/test_cases.c @@ -158,6 +158,7 @@ static const uint8_t correctSeq[5] = { 0, 1, 2, 1, 0 }; #define MAX_INPUTS 5 #define MAX_ATTEMPTS 3 #define RECOVERY_WINDOW_MS 5000 + static uint8_t inputSeq[MAX_INPUTS]; static uint8_t inputCount; static uint8_t attempts; @@ -167,88 +168,106 @@ static bool lastWasC; static TestCaseState tc5Outcome; static void tc5_init(void) { - inputCount = 0; - attempts = 0; - inRecovery = false; - recoveryStart = 0; - lastWasC = false; - tc5Outcome = TC_IN_PROGRESS; - tcResults[currentTest] = TC_IN_PROGRESS; - setTestCaseResult(tcResults); + inputCount = 0; + attempts = 0; + inRecovery = false; + recoveryStart = 0; + lastWasC = false; + tc5Outcome = TC_IN_PROGRESS; + + tcResults[currentTest] = TC_IN_PROGRESS; + setTestCaseResult(tcResults); } static void tc5_update(void) { - if (inRecovery && (msTicks - recoveryStart >= RECOVERY_WINDOW_MS)) { - attempts++; - tc5Outcome = (attempts >= MAX_ATTEMPTS) ? TC_FAIL : TC_FAIL; - inRecovery = false; - tcResults[currentTest] = tc5Outcome; - setTestCaseResult(tcResults); - return; - } - for (uint8_t i = 0; i < 3; i++) { - if (buttons[i].justPressed) { - buttons[i].justPressed = false; - if (inRecovery) { - if (i == 2) { - if (lastWasC) { - inRecovery = false; - inputCount = 0; - lastWasC = false; - tc5Outcome = TC_IN_PROGRESS; - tcResults[currentTest] = TC_IN_PROGRESS; - setTestCaseResult(tcResults); - return; - } else { - lastWasC = true; - } - } else { - lastWasC = false; - } - return; - } - if (inputCount < MAX_INPUTS) { - inputSeq[inputCount++] = i; - if (inputCount == MAX_INPUTS) { - bool correct = true; - for (uint8_t j = 0; j < MAX_INPUTS; j++) { - if (inputSeq[j] != correctSeq[j]) { - correct = false; - break; - } - } - if (correct) { - tc5Outcome = TC_PASS; - tcResults[currentTest] = TC_PASS; - setTestCaseResult(tcResults); - } else { - attempts++; - if (attempts >= MAX_ATTEMPTS) { - tc5Outcome = TC_FAIL; - tcResults[currentTest] = TC_FAIL; - setTestCaseResult(tcResults); - } else { - inRecovery = true; - recoveryStart = msTicks; - lastWasC = false; - tc5Outcome = TC_RETRY; - tcResults[currentTest] = TC_RETRY; - setTestCaseResult(tcResults); - } - } - } - } - } - } + // Handle recovery timeout + if (inRecovery && (msTicks - recoveryStart >= RECOVERY_WINDOW_MS)) { + attempts++; + inRecovery = false; + lastWasC = false; + + // Set internal outcome only; defer LED signaling to tc5_eval + tc5Outcome = TC_FAIL; + return; + } + + // Handle button presses + for (uint8_t i = 0; i < 3; i++) { + if (buttons[i].justPressed) { + buttons[i].justPressed = false; + + // Recovery mode logic + if (inRecovery) { + if (i == 2) { // Button C + if (lastWasC) { + // Successful recovery: reset state and retry + inRecovery = false; + inputCount = 0; + lastWasC = false; + tc5Outcome = TC_IN_PROGRESS; + + tcResults[currentTest] = TC_IN_PROGRESS; + setTestCaseResult(tcResults); + return; + } else { + lastWasC = true; + } + } else { + lastWasC = false; + } + return; + } + + // Normal input sequence + if (inputCount < MAX_INPUTS) { + inputSeq[inputCount++] = i; + + if (inputCount == MAX_INPUTS) { + bool correct = true; + for (uint8_t j = 0; j < MAX_INPUTS; j++) { + if (inputSeq[j] != correctSeq[j]) { + correct = false; + break; + } + } + + if (correct) { + tc5Outcome = TC_PASS; + } else { + attempts++; + if (attempts >= MAX_ATTEMPTS) { + tc5Outcome = TC_FAIL; + } else { + inRecovery = true; + recoveryStart = msTicks; + lastWasC = false; + tc5Outcome = TC_RETRY; + + tcResults[currentTest] = TC_RETRY; + setTestCaseResult(tcResults); + } + } + } + } + } + } } static TestCaseState tc5_eval(void) { - if (tc5Outcome == TC_IN_PROGRESS) { - return TC_FAIL; - } - return tc5Outcome; + // Final outcome only resolved here + if (tc5Outcome == TC_PASS || tc5Outcome == TC_FAIL) { + tcResults[currentTest] = tc5Outcome; + setTestCaseResult(tcResults); + return tc5Outcome; + } + + // If still in progress or retry, treat as failure on timeout + tcResults[currentTest] = TC_FAIL; + setTestCaseResult(tcResults); + return TC_FAIL; } +// ===== Test Case Framework ===== typedef struct { uint32_t durationMs; void (*initFn)(void); @@ -256,15 +275,19 @@ typedef struct { TestCaseState (*evalFn)(void); } TestCaseDef; +// ===== Test Case List ===== + static const TestCaseDef testCases[] = { { 5000, tc1_init, tc1_update, tc1_eval }, { 5000, tc2_init, tc2_update, tc2_eval }, { 5000, tc3_init, tc3_update, tc3_eval }, { 5000, tc4_init, tc4_update, tc4_eval }, - // { 10000, tc5_init, tc5_update, tc5_eval } + { 10000, tc5_init, tc5_update, tc5_eval } }; static const size_t NUM_TEST_CASES = sizeof(testCases) / sizeof(testCases[0]); +// ===== Test Case Runner ===== + void test_cases_start(uint8_t idx) { if (idx == 0) { for (size_t i = 0; i < NUM_TEST_CASES; i++) { From c889909bc0b4b7b416887d8915e699ef37b9f607 Mon Sep 17 00:00:00 2001 From: Mark Benson Date: Fri, 12 Sep 2025 17:49:10 +0100 Subject: [PATCH 12/12] Add early exit for TC5 If solution is correct, or all retries are exhausted, exit test early --- .../firmware/tester_runtime/src/test_cases.c | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/software/firmware/tester_runtime/src/test_cases.c b/software/firmware/tester_runtime/src/test_cases.c index 1bab8a9..3e4cc6e 100644 --- a/software/firmware/tester_runtime/src/test_cases.c +++ b/software/firmware/tester_runtime/src/test_cases.c @@ -9,6 +9,7 @@ #include #include +static bool testCaseShouldEndEarly = false; TestCaseState tcResults[5] = { TC_NO_RESULT }; uint8_t currentTest = 0; bool testActive = false; @@ -233,10 +234,14 @@ static void tc5_update(void) { if (correct) { tc5Outcome = TC_PASS; + testCaseShouldEndEarly = true; + return; } else { attempts++; if (attempts >= MAX_ATTEMPTS) { tc5Outcome = TC_FAIL; + testCaseShouldEndEarly = true; + return; } else { inRecovery = true; recoveryStart = msTicks; @@ -282,7 +287,7 @@ static const TestCaseDef testCases[] = { { 5000, tc2_init, tc2_update, tc2_eval }, { 5000, tc3_init, tc3_update, tc3_eval }, { 5000, tc4_init, tc4_update, tc4_eval }, - { 10000, tc5_init, tc5_update, tc5_eval } + { 30000, tc5_init, tc5_update, tc5_eval } }; static const size_t NUM_TEST_CASES = sizeof(testCases) / sizeof(testCases[0]); @@ -294,6 +299,7 @@ void test_cases_start(uint8_t idx) { tcResults[i] = TC_NO_RESULT; } } + testCaseShouldEndEarly = false; testStartTime = msTicks; testActive = true; runStatus(true); @@ -312,11 +318,14 @@ void test_cases_end(void) { } void test_cases_monitor_inputs(void) { - if (!testActive) return; - testCases[currentTest].updateFn(); - if ((uint32_t)(msTicks - testStartTime) >= testCases[currentTest].durationMs) { - test_cases_end(); - } + if (!testActive) return; + + testCases[currentTest].updateFn(); + + if (testCaseShouldEndEarly || + (uint32_t)(msTicks - testStartTime) >= testCases[currentTest].durationMs) { + test_cases_end(); + } } void test_cases_init(void) {}