// --------------------------------------------------------------------------- // Three weapon fencing scoring box - WeAct Black Pill, STM32F401CCU6 // // Board: "Generic STM32F4 series" -> BlackPill F401CC (STM32duino core) // Upload: STM32CubeProgrammer (DFU), or ST-Link // // What this does that the common Arduino designs do not: // 1. Reads the piste and both C lines, so floor hits are suppressed. // This is the difference between a bench toy and a box you can use on // a metal strip. // 2. Drives the two weapon lines from GPIO rather than a fixed pull-up and // alternates them, which makes blade-to-blade contact visible and so // makes sabre whip-over suppression possible at all. // 3. Measures lockout from hit registration, not from the start of contact. // 4. Never blocks. The reference sketch sits in delay() for 3.1 s after // every hit. // // All scoring decisions live in scoring_logic.cpp, which is plain C++ and is // unit tested on a PC. This file is only plumbing. // --------------------------------------------------------------------------- #include "scoring_logic.h" // ------------------------------- pin map ---------------------------------- // Analog inputs are ADC1_IN0..IN6 on PA0..PA6. Do not move them without also // changing the channel numbers in CH_* below. #define PIN_WA PA0 // fencer A, B line (weapon) #define PIN_LA PA1 // fencer A, A line (lame / epee return) #define PIN_GA PA2 // fencer A, C line (ground / guard) #define PIN_WB PA3 #define PIN_LB PA4 #define PIN_GB PA5 #define PIN_PIS PA6 // piste static const uint8_t CH_WA = 0, CH_LA = 1, CH_GA = 2; static const uint8_t CH_WB = 3, CH_LB = 4, CH_GB = 5, CH_PIS = 6; #define PIN_DRIVE_A PB0 // 1k from here to the fencer A weapon node #define PIN_DRIVE_B PB1 // 1k from here to the fencer B weapon node #define PIN_ON_A PB12 // green #define PIN_OFF_A PB13 // white #define PIN_OFF_B PB14 // white #define PIN_ON_B PB15 // red #define PIN_FAULT_A PA8 #define PIN_FAULT_B PA9 #define PIN_MODE_F PB3 #define PIN_MODE_E PB4 #define PIN_MODE_S PB5 #define PIN_BUZZER PB10 #define PIN_BTN_MODE PB6 #define PIN_BTN_RST PB7 #define PIN_HEARTBEAT PC13 // ----------------------------- behaviour ---------------------------------- static const uint32_t BUZZER_MS = 1000; static const uint32_t LIGHT_MS = 3000; static const uint32_t SETTLE_US = 5; // let the 1k / cable RC settle static const uint32_t DEBOUNCE_MS = 200; // Hold the mode button at power-up to freeze the drive phase on fencer A. // The hit simulator needs a fixed phase; see hit_simulator_nano.ino. static bool testMode = false; // ------------------------------ fast ADC ---------------------------------- // STM32duino analogRead() reconfigures the peripheral on every call and costs // 10-20 us. Direct register access gives about 1.3 us per channel, so a full // seven channel scan takes roughly 9 us instead of 100. static void adcInit() { RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; ADC->CCR = (ADC->CCR & ~ADC_CCR_ADCPRE) | (1u << ADC_CCR_ADCPRE_Pos); // PCLK2/4 = 21 MHz ADC1->CR1 = 0; // 12 bit, single conversion ADC1->CR2 = ADC_CR2_ADON; ADC1->SMPR2 = 0; for (uint8_t ch = 0; ch <= 6; ++ch) // 15 cycle sampling, ample for 1k source ADC1->SMPR2 |= (1u << (ch * 3)); delayMicroseconds(10); } static inline uint16_t adcRead(uint8_t ch) { ADC1->SQR3 = ch; ADC1->CR2 |= ADC_CR2_SWSTART; while (!(ADC1->SR & ADC_SR_EOC)) { } return (uint16_t)ADC1->DR; } // ------------------------------- state ------------------------------------ static ScoringBox box; static Contacts lastA, lastB; static Weapon weapon = EPEE; // foil with nothing plugged in buzzes forever static bool latched = false; static uint32_t latchMs = 0; static Lights shown; static uint32_t lastModeMs = 0, lastRstMs = 0; static uint32_t scans = 0, scanReportMs = 0; // --------------------------- classification ------------------------------- // Build the contact set for whichever fencer is currently driven high. // During fencer A's phase, driveB is held low, so fencer B's weapon node acts // as a pull-down and blade-to-blade contact shows up as a mid-rail pair. static void sampleSelf(bool phaseA, Contacts& c) { uint16_t w = adcRead(phaseA ? CH_WA : CH_WB); uint16_t l = adcRead(phaseA ? CH_LA : CH_LB); uint16_t g = adcRead(phaseA ? CH_GA : CH_GB); uint16_t ol = adcRead(phaseA ? CH_LB : CH_LA); uint16_t og = adcRead(phaseA ? CH_GB : CH_GA); uint16_t ow = adcRead(phaseA ? CH_WB : CH_WA); uint16_t pis = adcRead(CH_PIS); c.toOwnGround = joined(w, g); c.toOwnLame = joined(w, l); c.toOppLame = joined(w, ol); c.toOppGround = joined(w, og); c.toOppWeapon = joined(w, ow); c.toPiste = joined(w, pis); c.floating = (levelOf(w) == LVL_HIGH); } // ------------------------------- output ----------------------------------- static void showModeLeds() { digitalWrite(PIN_MODE_F, weapon == FOIL); digitalWrite(PIN_MODE_E, weapon == EPEE); digitalWrite(PIN_MODE_S, weapon == SABRE); } static void allLampsOff() { digitalWrite(PIN_ON_A, LOW); digitalWrite(PIN_OFF_A, LOW); digitalWrite(PIN_ON_B, LOW); digitalWrite(PIN_OFF_B, LOW); digitalWrite(PIN_BUZZER, LOW); } static void clearLatch() { latched = false; allLampsOff(); box.reset(); } // ------------------------------- setup ------------------------------------ void setup() { Serial.begin(115200); pinMode(PIN_WA, INPUT_ANALOG); pinMode(PIN_LA, INPUT_ANALOG); pinMode(PIN_GA, INPUT_ANALOG); pinMode(PIN_WB, INPUT_ANALOG); pinMode(PIN_LB, INPUT_ANALOG); pinMode(PIN_GB, INPUT_ANALOG); pinMode(PIN_PIS, INPUT_ANALOG); pinMode(PIN_DRIVE_A, OUTPUT); pinMode(PIN_DRIVE_B, OUTPUT); pinMode(PIN_ON_A, OUTPUT); pinMode(PIN_OFF_A, OUTPUT); pinMode(PIN_ON_B, OUTPUT); pinMode(PIN_OFF_B, OUTPUT); pinMode(PIN_FAULT_A, OUTPUT); pinMode(PIN_FAULT_B, OUTPUT); pinMode(PIN_MODE_F, OUTPUT); pinMode(PIN_MODE_E, OUTPUT); pinMode(PIN_MODE_S, OUTPUT); pinMode(PIN_BUZZER, OUTPUT); pinMode(PIN_HEARTBEAT, OUTPUT); pinMode(PIN_BTN_MODE, INPUT_PULLUP); pinMode(PIN_BTN_RST, INPUT_PULLUP); adcInit(); lastA.clear(); lastB.clear(); testMode = (digitalRead(PIN_BTN_MODE) == LOW); // lamp test for (int p : {PIN_ON_A, PIN_OFF_A, PIN_OFF_B, PIN_ON_B, PIN_FAULT_A, PIN_FAULT_B}) { digitalWrite(p, HIGH); delay(120); digitalWrite(p, LOW); } if (testMode) { // Wait for the held mode button to be released, otherwise the loop's // mode handler reads the same press and cycles the weapon every 200 ms. while (digitalRead(PIN_BTN_MODE) == LOW) { delay(10); } lastModeMs = millis(); } box.setWeapon(weapon); showModeLeds(); Serial.println(F("# three weapon scoring box, F401")); Serial.print (F("# weapon: ")); Serial.println((int)weapon); if (testMode) Serial.println(F("# TEST MODE: drive phase frozen on fencer A")); } // -------------------------------- loop ------------------------------------ void loop() { static bool phaseA = true; // ---- drive the phase and let the network settle ---- if (testMode) { digitalWrite(PIN_DRIVE_A, HIGH); digitalWrite(PIN_DRIVE_B, LOW); phaseA = true; } else { digitalWrite(PIN_DRIVE_A, phaseA ? HIGH : LOW); digitalWrite(PIN_DRIVE_B, phaseA ? LOW : HIGH); } delayMicroseconds(SETTLE_US); if (phaseA) sampleSelf(true, lastA); else sampleSelf(false, lastB); Frame f; f.a = lastA; f.b = lastB; f.t_us = micros(); Lights L = box.update(f); ++scans; digitalWrite(PIN_FAULT_A, L.faultA); digitalWrite(PIN_FAULT_B, L.faultB); // ---- a hit has resolved: latch the lamps, do not block ---- if (L.signalled && !latched) { latched = true; latchMs = millis(); shown = L; digitalWrite(PIN_ON_A, L.onA); digitalWrite(PIN_OFF_A, L.offA); digitalWrite(PIN_ON_B, L.onB); digitalWrite(PIN_OFF_B, L.offB); digitalWrite(PIN_BUZZER, HIGH); Serial.print(F("HIT ")); Serial.print(L.onA ? "GH " : (L.offA ? "GM " : "-- ")); Serial.println(L.onB ? "RH" : (L.offB ? "RM" : "--")); } if (latched) { uint32_t age = millis() - latchMs; if (age >= BUZZER_MS) digitalWrite(PIN_BUZZER, LOW); if (age >= LIGHT_MS) clearLatch(); } // ---- buttons ---- if (digitalRead(PIN_BTN_MODE) == LOW && millis() - lastModeMs > DEBOUNCE_MS) { lastModeMs = millis(); weapon = (Weapon)((weapon + 1) % 3); box.setWeapon(weapon); showModeLeds(); allLampsOff(); latched = false; Serial.print(F("# weapon: ")); Serial.println((int)weapon); } if (digitalRead(PIN_BTN_RST) == LOW && millis() - lastRstMs > DEBOUNCE_MS) { lastRstMs = millis(); clearLatch(); } if (!testMode) phaseA = !phaseA; // ---- once a second, report the achieved scan rate ---- if (millis() - scanReportMs >= 1000) { scanReportMs = millis(); digitalWrite(PIN_HEARTBEAT, !digitalRead(PIN_HEARTBEAT)); Serial.print(F("# scans/s: ")); Serial.println(scans); scans = 0; } }