From 12ea8e3e44669a1111294145d0a5bcc04866d4ac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 27 Oct 2026 19:01:38 +0000 Subject: [PATCH] Bring up the firmware2 HAL for bench 1 and 2. Drive the section 2.2 relay codes as GPIO, sample TX_ISENSE and the CC pins, and hold the PA off until the soft-start gates pass. The sketch adds the SH1106, buttons, SYNC LED, a fixed CLK0 carrier, the MCP4725, and a 24AA64 presence check. Relay PWM stays off; the timer map is the DS40002183E check. Co-authored-by: Jason Milldrum --- .github/workflows/firmware2.yml | 3 - firmware2/OpenBeacon2/OpenBeacon2.ino | 187 ++++++++++++++-- firmware2/OpenBeacon2/ob2_bus.h | 16 ++ firmware2/OpenBeacon2/ob2_dac.cpp | 9 + firmware2/OpenBeacon2/ob2_dac.h | 20 ++ firmware2/OpenBeacon2/ob2_display.cpp | 124 +++++++++++ firmware2/OpenBeacon2/ob2_hal.cpp | 262 ++++++++++++++++++++++ firmware2/OpenBeacon2/ob2_hal.h | 32 +++ firmware2/OpenBeacon2/ob2_hold.cpp | 30 +++ firmware2/OpenBeacon2/ob2_hold.h | 20 ++ firmware2/OpenBeacon2/ob2_power.cpp | 45 ++++ firmware2/OpenBeacon2/ob2_power.h | 32 +++ firmware2/OpenBeacon2/ob2_pwm.h | 52 +++++ firmware2/OpenBeacon2/ob2_relays.cpp | 129 +++++++++++ firmware2/OpenBeacon2/ob2_relays.h | 54 +++++ firmware2/OpenBeacon2/ob2_softstart.cpp | 12 + firmware2/OpenBeacon2/ob2_softstart.h | 16 ++ firmware2/OpenBeacon2/ob2_ui.h | 42 ++++ firmware2/README.md | 100 ++++++--- firmware2/host/CMakeLists.txt | 11 +- firmware2/host/test_hal.cpp | 279 ++++++++++++++++++++++++ firmware2/tools/build.sh | 7 + 22 files changed, 1434 insertions(+), 48 deletions(-) create mode 100644 firmware2/OpenBeacon2/ob2_bus.h create mode 100644 firmware2/OpenBeacon2/ob2_dac.cpp create mode 100644 firmware2/OpenBeacon2/ob2_dac.h create mode 100644 firmware2/OpenBeacon2/ob2_display.cpp create mode 100644 firmware2/OpenBeacon2/ob2_hal.cpp create mode 100644 firmware2/OpenBeacon2/ob2_hal.h create mode 100644 firmware2/OpenBeacon2/ob2_hold.cpp create mode 100644 firmware2/OpenBeacon2/ob2_hold.h create mode 100644 firmware2/OpenBeacon2/ob2_power.cpp create mode 100644 firmware2/OpenBeacon2/ob2_power.h create mode 100644 firmware2/OpenBeacon2/ob2_pwm.h create mode 100644 firmware2/OpenBeacon2/ob2_relays.cpp create mode 100644 firmware2/OpenBeacon2/ob2_relays.h create mode 100644 firmware2/OpenBeacon2/ob2_softstart.cpp create mode 100644 firmware2/OpenBeacon2/ob2_softstart.h create mode 100644 firmware2/OpenBeacon2/ob2_ui.h create mode 100644 firmware2/host/test_hal.cpp diff --git a/.github/workflows/firmware2.yml b/.github/workflows/firmware2.yml index 11a05dbd..736ff493 100644 --- a/.github/workflows/firmware2.yml +++ b/.github/workflows/firmware2.yml @@ -37,9 +37,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install CMake - run: sudo apt-get update && sudo apt-get install -y cmake g++ - - name: Test run: | cmake -S firmware2/host -B firmware2/host/build -DCMAKE_CXX_COMPILER=g++ diff --git a/firmware2/OpenBeacon2/OpenBeacon2.ino b/firmware2/OpenBeacon2/OpenBeacon2.ino index 03c73bb6..3cf411e1 100644 --- a/firmware2/OpenBeacon2/OpenBeacon2.ino +++ b/firmware2/OpenBeacon2/OpenBeacon2.ino @@ -1,29 +1,180 @@ /* - * OpenBeacon 2 firmware2, milestone M0. + * OpenBeacon 2 firmware2, milestone M1 bring-up. * - * Builds for the AVR128DA48 on DxCore. Holds the relay drivers and the SYNC - * LED off so a later bring-up starts from the reset state in spec section 2.2 - * (BAND_C low; R91 pull-down on the rework, which is not in this repo yet). - * No radio, GPS, display, or protocol code. + * Optiboot image and fuse notes are in firmware2/README.md (Bench 1). + * This sketch does not burn fuses. It does not call delay(). Relays are + * GPIO, fully on. PA4 stays low until DSP1, and only after the 50 ms + * power-up gate and a CC sample. Bias and CLK0 wait until the boost has + * been on for 20 ms. Changing filter or pressing BACK/DSP2 drops the + * carrier before the relays move. * - * Clock: the FQBN selects 24 MHz internal OSCHF. DxCore writes FREQSEL only. - * It does not set CLKCTRL.OSCHFCTRLA.AUTOTUNE, so this stub is not disciplined - * to the 32.768 kHz crystal. That belongs with the timebase, not M0. + * Clock: FQBN clock=24internal still does not set OSCHF autotune. + * millis stays on TCB2. USART2 and USART3 stay off (GPS is later). */ -#include "ob2_pins.h" +#include "ob2_hal.h" -void setup() { - pinMode(OB2_BAND_A, OUTPUT); - pinMode(OB2_BAND_B, OUTPUT); - pinMode(OB2_BAND_C, OUTPUT); - digitalWrite(OB2_BAND_A, LOW); - digitalWrite(OB2_BAND_B, LOW); - digitalWrite(OB2_BAND_C, LOW); +enum class Carrier : uint8_t { Off, BoostWait, On }; + +static Carrier g_carrier = Carrier::Off; +static uint8_t g_filter = 0; +static uint32_t g_boost_ms = 0; +static uint32_t g_last_adc_ms = 0; +static uint32_t g_last_draw_ms = 0; +static bool g_draw = true; +static uint16_t g_isense_counts = 0; +static uint16_t g_cc1_counts = 0; +static uint16_t g_cc2_counts = 0; +static Ob2Hold g_cc_hold; +static Ob2Hold g_btn[Ob2BtnCount]; + +static Ob2Filter filter_now() { + return static_cast(g_filter); +} + +static void stop_carrier() { + ob2_bias_mv(0); + ob2_carrier(0, false); + ob2_boost(false); + g_carrier = Carrier::Off; +} - pinMode(OB2_SYNC_LED, OUTPUT); - digitalWrite(OB2_SYNC_LED, LOW); +static void apply_filter(uint8_t index) { + if (g_carrier != Carrier::Off) { + stop_carrier(); + } + g_filter = static_cast(index % OB2_FILTER_COUNT); + ob2_relays_write(ob2_relay_for_filter(filter_now())); + g_draw = true; +} + +static void request_carrier(uint32_t now_ms) { + if (!ob2_synth_ok()) { + return; + } + if (!ob2_may_enable_boost(now_ms, ob2_cc_sampled())) { + return; + } + if (g_carrier != Carrier::Off) { + return; + } + ob2_boost(true); + g_boost_ms = now_ms; + g_carrier = Carrier::BoostWait; + g_draw = true; +} + +static void on_press(uint8_t button, uint32_t now_ms) { + if (button == Ob2BtnUp) { + apply_filter(static_cast(g_filter + 1)); + } else if (button == Ob2BtnDown) { + apply_filter(static_cast(g_filter + OB2_FILTER_COUNT - 1)); + } else if (button == Ob2BtnDsp1) { + request_carrier(now_ms); + } else if (button == Ob2BtnBack || button == Ob2BtnDsp2) { + if (g_carrier != Carrier::Off) { + stop_carrier(); + g_draw = true; + } + } +} + +static void service_carrier(uint32_t now_ms) { + if (g_carrier != Carrier::BoostWait) { + return; + } + if (!ob2_may_ramp_bias(ob2_boost_is_on(), now_ms, g_boost_ms)) { + return; + } + const uint32_t hz = ob2_filter_carrier_hz(filter_now()); + ob2_bias_mv(OB2_BIAS_BRINGUP_MV); + ob2_carrier(hz, true); + g_carrier = Carrier::On; + g_draw = true; +} + +static Ob2Ui make_ui(uint32_t now_ms) { + const Ob2Relay relay = ob2_relay_for_filter(filter_now()); + const uint16_t cc1_mv = ob2_adc_counts_to_mv(g_cc1_counts); + const uint16_t cc2_mv = ob2_adc_counts_to_mv(g_cc2_counts); + const uint16_t sense_mv = ob2_adc_counts_to_mv(g_isense_counts); + const uint16_t ma = ob2_isense_ma(sense_mv); + Ob2Ui ui = {}; + ui.filter_name = ob2_filter_name(filter_now()); + ui.filter_bands = ob2_filter_bands(filter_now()); + ui.a = relay.a; + ui.b = relay.b; + ui.c = relay.c; + ui.coils = relay.coils; + ui.load_ma = relay.load_ma; + ui.cc_raw = ob2_cc_classify_mv(cc1_mv, cc2_mv); + ui.cc_stable = static_cast(g_cc_hold.stable); + ui.cc_stable_valid = g_cc_hold.stable_valid; + ui.cc1_mv = cc1_mv; + ui.cc2_mv = cc2_mv; + ui.isense_ma = ma; + ui.isense_spec = ob2_isense_specified(ma); + ui.may_key = ob2_may_enable_boost(now_ms, ob2_cc_sampled()); + ui.boost = g_carrier != Carrier::Off; + ui.bias_live = g_carrier == Carrier::On; + ui.synth_ok = ob2_synth_ok(); + ui.display_ok = ob2_display_ok(); + ui.eeprom_ok = ob2_eeprom_ok(); + ui.eeprom_busy = ob2_eeprom_busy(); + ui.carrier_hz = ob2_filter_carrier_hz(filter_now()); + ui.buttons = 0; + for (uint8_t i = 0; i < Ob2BtnCount; ++i) { + if (ob2_button_raw(i)) { + ui.buttons = static_cast(ui.buttons | (1u << i)); + } + } + return ui; +} + +void setup() { + ob2_hal_begin(); + ob2_hold_init(&g_cc_hold); + for (uint8_t i = 0; i < Ob2BtnCount; ++i) { + ob2_hold_init(&g_btn[i]); + } + apply_filter(0); + g_last_adc_ms = millis(); + ob2_adc_sample(&g_isense_counts, &g_cc1_counts, &g_cc2_counts); + const Ob2CcLevel raw = ob2_cc_classify_mv(ob2_adc_counts_to_mv(g_cc1_counts), + ob2_adc_counts_to_mv(g_cc2_counts)); + ob2_hold_feed(&g_cc_hold, static_cast(raw), g_last_adc_ms, OB2_CC_DEBOUNCE_MS); } void loop() { + const uint32_t now_ms = millis(); + ob2_eeprom_poll(now_ms); + + if ((now_ms - g_last_adc_ms) >= 50) { + g_last_adc_ms = now_ms; + ob2_adc_sample(&g_isense_counts, &g_cc1_counts, &g_cc2_counts); + const Ob2CcLevel raw = ob2_cc_classify_mv(ob2_adc_counts_to_mv(g_cc1_counts), + ob2_adc_counts_to_mv(g_cc2_counts)); + if (ob2_hold_feed(&g_cc_hold, static_cast(raw), now_ms, OB2_CC_DEBOUNCE_MS)) { + g_draw = true; + } + } + + bool held = false; + for (uint8_t i = 0; i < Ob2BtnCount; ++i) { + const bool down = ob2_button_raw(i); + held = held || down; + if (ob2_hold_feed(&g_btn[i], down ? 1 : 0, now_ms, OB2_BUTTON_DEBOUNCE_MS) && + g_btn[i].stable == 1) { + on_press(i, now_ms); + } + } + ob2_sync(held || ((now_ms / 500U) & 1U) != 0); + + service_carrier(now_ms); + + if (g_draw || (now_ms - g_last_draw_ms) >= 200) { + g_last_draw_ms = now_ms; + g_draw = false; + ob2_display(make_ui(now_ms)); + } } diff --git a/firmware2/OpenBeacon2/ob2_bus.h b/firmware2/OpenBeacon2/ob2_bus.h new file mode 100644 index 00000000..833b26c6 --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_bus.h @@ -0,0 +1,16 @@ +#pragma once + +/* + * TWI0 on PA2/PA3. Si5351C is 0x60. MCP4725A1 is 0x62 on the 5 V side + * of the same bus. U8 is 24AA64T-I/OT in SOT-23-5: the symbol has SCL, + * VSS, SDA, VCC, and WP, and no A0/A1/A2, so the address is 0x50. + * The four magic bytes are a bring-up presence check, not the config schema. + */ + +#include + +constexpr uint8_t OB2_SI5351_ADDR = 0x60; +constexpr uint8_t OB2_EEPROM_ADDR = 0x50; +constexpr uint8_t OB2_EEPROM_MAGIC[] = {'O', 'B', '2', '1'}; +constexpr uint8_t OB2_EEPROM_MAGIC_LEN = 4; +constexpr uint32_t OB2_TWI_HZ = 100000UL; diff --git a/firmware2/OpenBeacon2/ob2_dac.cpp b/firmware2/OpenBeacon2/ob2_dac.cpp new file mode 100644 index 00000000..7a14e75c --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_dac.cpp @@ -0,0 +1,9 @@ +#include "ob2_dac.h" + +uint16_t ob2_mcp4725_code(uint16_t mv) { + if (mv >= OB2_MCP4725_VREF_MV) { + return OB2_MCP4725_MAX_CODE; + } + return static_cast((static_cast(mv) * OB2_MCP4725_FULL_SCALE) / + OB2_MCP4725_VREF_MV); +} diff --git a/firmware2/OpenBeacon2/ob2_dac.h b/firmware2/OpenBeacon2/ob2_dac.h new file mode 100644 index 00000000..74d743f7 --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_dac.h @@ -0,0 +1,20 @@ +#pragma once + +/* + * MCP4725A1 at 0x62, 5 V full scale. Code is (mV * 4096) / 5000, + * clamped to the 12-bit range. 5000 mV would be code 4096, which does + * not fit, so that input becomes 4095. + * + * OB2_BIAS_BRINGUP_MV is a conservative bench placeholder, not a + * calibrated power level. + */ + +#include + +constexpr uint8_t OB2_MCP4725_ADDR = 0x62; +constexpr uint16_t OB2_MCP4725_VREF_MV = 5000; +constexpr uint16_t OB2_MCP4725_FULL_SCALE = 4096; +constexpr uint16_t OB2_MCP4725_MAX_CODE = 4095; +constexpr uint16_t OB2_BIAS_BRINGUP_MV = 500; + +uint16_t ob2_mcp4725_code(uint16_t mv); diff --git a/firmware2/OpenBeacon2/ob2_display.cpp b/firmware2/OpenBeacon2/ob2_display.cpp new file mode 100644 index 00000000..6ac229be --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_display.cpp @@ -0,0 +1,124 @@ +#include "ob2_hal.h" + +#include +#include + +/* + * SH1106 128x64 on SPI1 ALT1 (PC4 MOSI, PC6 SCK, PC7 CS as GPIO, PB4 DC, PB5 RES). + * DxCore sets SPI_INTERFACES_COUNT to 1, so U8g2's 2nd-HW-SPI class compiles to a + * no-op. The one SPI object is swapped onto SPI1 ALT1 before begin(). SPI0 default + * pins are PA4-PA7 and must not be started. + */ + +static U8G2_SH1106_128X64_NONAME_1_4W_HW_SPI g_u8g2(U8G2_R0, OB2_OLED_CS, OB2_OLED_DC, OB2_OLED_RES); +static bool g_display_ok = false; + +bool ob2_display_ok() { + return g_display_ok; +} + +bool ob2_display_begin() { + pinMode(OB2_OLED_CS, OUTPUT); + digitalWrite(OB2_OLED_CS, HIGH); + pinMode(OB2_OLED_DC, OUTPUT); + pinMode(OB2_OLED_RES, OUTPUT); + if (!SPI.swap(SPI1_SWAP_ALT1)) { + g_display_ok = false; + return false; + } + g_u8g2.begin(); + g_display_ok = true; + return true; +} + +void ob2_display(const Ob2Ui& ui) { + if (!g_display_ok) { + return; + } + g_u8g2.firstPage(); + do { + g_u8g2.setFont(u8g2_font_5x8_tr); + + g_u8g2.setCursor(0, 8); + g_u8g2.print(F("OB2 ")); + g_u8g2.print(ui.filter_name); + g_u8g2.print(' '); + g_u8g2.print(ui.filter_bands); + + g_u8g2.setCursor(0, 16); + g_u8g2.print(F("ABC ")); + g_u8g2.print(ui.a); + g_u8g2.print(ui.b); + g_u8g2.print(ui.c); + g_u8g2.print(' '); + g_u8g2.print(ui.coils); + g_u8g2.print(F("c ")); + g_u8g2.print(ui.load_ma); + g_u8g2.print(F("mA")); + + g_u8g2.setCursor(0, 24); + g_u8g2.print(F("CC ")); + g_u8g2.print(ob2_cc_name(ui.cc_raw)); + g_u8g2.print(' '); + g_u8g2.print(ui.cc1_mv); + g_u8g2.print('/'); + g_u8g2.print(ui.cc2_mv); + + g_u8g2.setCursor(0, 32); + g_u8g2.print(F("deb ")); + if (ui.cc_stable_valid) { + g_u8g2.print(ob2_cc_name(ui.cc_stable)); + } else { + g_u8g2.print(F("...")); + } + g_u8g2.print(F(" I ")); + if (!ui.isense_spec) { + g_u8g2.print(F("<100")); + } else { + g_u8g2.print(ui.isense_ma); + } + + g_u8g2.setCursor(0, 40); + if (ui.bias_live) { + g_u8g2.print(F("TX ")); + g_u8g2.print(ui.carrier_hz); + g_u8g2.print(F("Hz")); + } else if (ui.boost) { + g_u8g2.print(F("boost settle")); + } else if (!ui.may_key) { + g_u8g2.print(F("key locked")); + } else if (!ui.synth_ok) { + g_u8g2.print(F("no synth")); + } else { + g_u8g2.print(F("key idle")); + } + + g_u8g2.setCursor(0, 48); + g_u8g2.print(F("EE ")); + if (ui.eeprom_busy) { + g_u8g2.print(F("wr")); + } else if (ui.eeprom_ok) { + g_u8g2.print(F("ok")); + } else { + g_u8g2.print(F("fail")); + } + g_u8g2.print(F(" OLED ")); + g_u8g2.print(ui.display_ok ? F("ok") : F("no")); + g_u8g2.print(F(" CLK ")); + g_u8g2.print(ui.bias_live ? F("on") : F("off")); + + g_u8g2.setCursor(0, 56); + g_u8g2.print(F("btn ")); + static const char kMarks[] = "UDLRB12"; + for (uint8_t i = 0; i < Ob2BtnCount; ++i) { + if ((ui.buttons & (1u << i)) != 0) { + g_u8g2.print(kMarks[i]); + } else { + g_u8g2.print('.'); + } + } + + g_u8g2.setCursor(0, 64); + g_u8g2.print(F("DSP1 on BACK off")); + } while (g_u8g2.nextPage()); +} diff --git a/firmware2/OpenBeacon2/ob2_hal.cpp b/firmware2/OpenBeacon2/ob2_hal.cpp new file mode 100644 index 00000000..0f39b045 --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_hal.cpp @@ -0,0 +1,262 @@ +#include "ob2_hal.h" +#include "ob2_pwm.h" + +#include +#include + +static_assert(!OB2_RELAY_PWM_SHIPPED, "release relays stay GPIO"); +static_assert(!OB2_TCA0_SERVES_PA5_AND_PB1, "TCA0 has one PORTMUX setting"); +static_assert(!OB2_TCB_DRIVES_RELAY_PINS, "no TCB waveform on PA5, PA6, or PB1"); + +static const uint8_t kButtonPins[Ob2BtnCount] = { + OB2_BTN_UP, OB2_BTN_DOWN, OB2_BTN_LEFT, OB2_BTN_RIGHT, + OB2_BTN_BACK, OB2_BTN_DSP_1, OB2_BTN_DSP_2, +}; + +static Si5351 g_si5351; +static bool g_synth_ok = false; +static bool g_boost_on = false; +static bool g_cc_sampled = false; +static bool g_eeprom_ok = false; +static bool g_eeprom_busy = false; +static uint32_t g_eeprom_write_ms = 0; + +static void relay_pin_low(uint8_t pin) { + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); +} + +static void clear_pullup(volatile uint8_t* ctrl) { + *ctrl = static_cast(*ctrl & ~PORT_PULLUPEN_bm); +} + +static bool eeprom_read(uint8_t* out) { + Wire.beginTransmission(OB2_EEPROM_ADDR); + Wire.write(static_cast(0)); + Wire.write(static_cast(0)); + if (Wire.endTransmission(false) != 0) { + return false; + } + if (Wire.requestFrom(static_cast(OB2_EEPROM_ADDR), + static_cast(OB2_EEPROM_MAGIC_LEN)) != OB2_EEPROM_MAGIC_LEN) { + return false; + } + for (uint8_t i = 0; i < OB2_EEPROM_MAGIC_LEN; ++i) { + out[i] = static_cast(Wire.read()); + } + return true; +} + +static bool eeprom_magic_ok(const uint8_t* bytes) { + for (uint8_t i = 0; i < OB2_EEPROM_MAGIC_LEN; ++i) { + if (bytes[i] != OB2_EEPROM_MAGIC[i]) { + return false; + } + } + return true; +} + +static void eeprom_start_write(uint32_t now_ms) { + Wire.beginTransmission(OB2_EEPROM_ADDR); + Wire.write(static_cast(0)); + Wire.write(static_cast(0)); + for (uint8_t i = 0; i < OB2_EEPROM_MAGIC_LEN; ++i) { + Wire.write(OB2_EEPROM_MAGIC[i]); + } + if (Wire.endTransmission() != 0) { + g_eeprom_ok = false; + g_eeprom_busy = false; + return; + } + g_eeprom_busy = true; + g_eeprom_ok = false; + g_eeprom_write_ms = now_ms; +} + +static void gpio_begin() { + relay_pin_low(OB2_BAND_A); + relay_pin_low(OB2_BAND_B); + relay_pin_low(OB2_BAND_C); + /* Section 4.5 still says pull PB1 up. PB1 is BAND_C with R91 to ground. */ + clear_pullup(&PORTA.PIN5CTRL); + clear_pullup(&PORTA.PIN6CTRL); + clear_pullup(&PORTB.PIN1CTRL); + + relay_pin_low(OB2_TX); + clear_pullup(&PORTA.PIN4CTRL); + g_boost_on = false; + + pinMode(OB2_SYNC_LED, OUTPUT); + digitalWrite(OB2_SYNC_LED, LOW); + + for (uint8_t i = 0; i < Ob2BtnCount; ++i) { + pinMode(kButtonPins[i], INPUT); + } +} + +static void adc_begin() { + /* DxCore init leaves the ADC in 10-bit mode on VDD. Section 4.14 wants 12-bit and 2.5 V. */ + VREF.ADC0REF = static_cast((VREF.ADC0REF & ~VREF_REFSEL_gm) | VREF_REFSEL_2V500_gc); + ADC0.CTRLA = static_cast((ADC0.CTRLA & ~ADC_RESSEL_gm) | ADC_ENABLE_bm | ADC_RESSEL_12BIT_gc); + PORTE.PIN1CTRL = PORT_ISC_INPUT_DISABLE_gc; + PORTE.PIN2CTRL = PORT_ISC_INPUT_DISABLE_gc; + PORTE.PIN3CTRL = PORT_ISC_INPUT_DISABLE_gc; +} + +static uint16_t adc_convert(uint8_t channel) { + ADC0.MUXPOS = static_cast(channel << ADC_MUXPOS_gp); + ADC0.INTFLAGS = ADC_RESRDY_bm; + ADC0.COMMAND = ADC_STCONV_bm; + while ((ADC0.INTFLAGS & ADC_RESRDY_bm) == 0) { + } + /* DA-series erratum: point the mux at a spare channel before reading RES. */ + ADC0.MUXPOS = 0x40; + return ADC0.RES; +} + +void ob2_adc_sample(uint16_t* isense_counts, uint16_t* cc1_counts, uint16_t* cc2_counts) { + const uint16_t isense = adc_convert(static_cast(OB2_AIN_TX_ISENSE)); + const uint16_t cc1 = adc_convert(static_cast(OB2_AIN_CC1_SENSE)); + const uint16_t cc2 = adc_convert(static_cast(OB2_AIN_CC2_SENSE)); + g_cc_sampled = true; + if (isense_counts != nullptr) { + *isense_counts = isense; + } + if (cc1_counts != nullptr) { + *cc1_counts = cc1; + } + if (cc2_counts != nullptr) { + *cc2_counts = cc2; + } +} + +bool ob2_cc_sampled() { + return g_cc_sampled; +} + +void ob2_relays_write(Ob2Relay relay) { + if (!relay.legal) { + relay.a = 0; + relay.b = 0; + relay.c = 0; + } + digitalWrite(OB2_BAND_A, relay.a ? HIGH : LOW); + digitalWrite(OB2_BAND_B, relay.b ? HIGH : LOW); + digitalWrite(OB2_BAND_C, relay.c ? HIGH : LOW); +} + +void ob2_boost(bool on) { + digitalWrite(OB2_TX, on ? HIGH : LOW); + g_boost_on = on; +} + +bool ob2_boost_is_on() { + return g_boost_on; +} + +void ob2_sync(bool on) { + digitalWrite(OB2_SYNC_LED, on ? HIGH : LOW); +} + +bool ob2_button_raw(uint8_t index) { + if (index >= Ob2BtnCount) { + return false; + } + return digitalRead(kButtonPins[index]) == LOW; +} + +void ob2_bias_mv(uint16_t mv) { + const uint16_t code = ob2_mcp4725_code(mv); + Wire.beginTransmission(OB2_MCP4725_ADDR); + Wire.write(static_cast((code >> 8) & 0x0F)); + Wire.write(static_cast(code & 0xFF)); + Wire.endTransmission(); +} + +static void eeprom_begin(uint32_t now_ms) { + uint8_t bytes[OB2_EEPROM_MAGIC_LEN] = {0, 0, 0, 0}; + if (eeprom_read(bytes) && eeprom_magic_ok(bytes)) { + g_eeprom_ok = true; + g_eeprom_busy = false; + return; + } + eeprom_start_write(now_ms); +} + +void ob2_eeprom_poll(uint32_t now_ms) { + if (!g_eeprom_busy) { + return; + } + Wire.beginTransmission(OB2_EEPROM_ADDR); + const uint8_t err = Wire.endTransmission(); + if (err != 0) { + if ((now_ms - g_eeprom_write_ms) > 10) { + g_eeprom_busy = false; + g_eeprom_ok = false; + } + return; + } + uint8_t bytes[OB2_EEPROM_MAGIC_LEN] = {0, 0, 0, 0}; + g_eeprom_busy = false; + g_eeprom_ok = eeprom_read(bytes) && eeprom_magic_ok(bytes); +} + +bool ob2_eeprom_ok() { + return g_eeprom_ok; +} + +bool ob2_eeprom_busy() { + return g_eeprom_busy; +} + +static void synth_begin() { + g_synth_ok = g_si5351.init(SI5351_CRYSTAL_LOAD_0PF, 25000000UL, 0); + if (!g_synth_ok) { + return; + } + /* 8 mA is the old CLK0 drive. Section 4.4 does not restate it. */ + g_si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_8MA); + g_si5351.output_enable(SI5351_CLK0, 0); + g_si5351.output_enable(SI5351_CLK2, 0); + g_si5351.output_enable(SI5351_CLK4, 0); +} + +bool ob2_synth_ok() { + return g_synth_ok; +} + +void ob2_carrier(uint32_t hz, bool on) { + if (!g_synth_ok) { + return; + } + if (!on) { + g_si5351.output_enable(SI5351_CLK0, 0); + return; + } + g_si5351.set_freq(static_cast(hz) * 100ULL, SI5351_CLK0); + g_si5351.output_enable(SI5351_CLK0, 1); +} + +void ob2_hal_begin() { + gpio_begin(); + adc_begin(); + + Wire.begin(); + Wire.setClock(OB2_TWI_HZ); + ob2_bias_mv(0); + + const uint32_t now = millis(); + eeprom_begin(now); + synth_begin(); + Wire.setClock(OB2_TWI_HZ); + const bool display = ob2_display_begin(); + ob2_adc_sample(nullptr, nullptr, nullptr); + + Serial.begin(115200); + Serial.print(F("OB2 M1 synth=")); + Serial.print(g_synth_ok ? F("ok") : F("no")); + Serial.print(F(" oled=")); + Serial.print(display ? F("ok") : F("no")); + Serial.print(F(" ee=")); + Serial.println(g_eeprom_busy ? F("wr") : (g_eeprom_ok ? F("ok") : F("fail"))); +} diff --git a/firmware2/OpenBeacon2/ob2_hal.h b/firmware2/OpenBeacon2/ob2_hal.h new file mode 100644 index 00000000..6ebeb92f --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_hal.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include "ob2_bus.h" +#include "ob2_dac.h" +#include "ob2_hold.h" +#include "ob2_pins.h" +#include "ob2_power.h" +#include "ob2_relays.h" +#include "ob2_softstart.h" +#include "ob2_ui.h" + +void ob2_hal_begin(); +bool ob2_display_begin(); + +bool ob2_display_ok(); +bool ob2_synth_ok(); +bool ob2_eeprom_ok(); +bool ob2_eeprom_busy(); +bool ob2_cc_sampled(); +bool ob2_boost_is_on(); + +void ob2_eeprom_poll(uint32_t now_ms); +void ob2_relays_write(Ob2Relay relay); +void ob2_boost(bool on); +void ob2_bias_mv(uint16_t mv); +void ob2_carrier(uint32_t hz, bool on); +bool ob2_button_raw(uint8_t index); +void ob2_sync(bool on); +void ob2_adc_sample(uint16_t* isense_counts, uint16_t* cc1_counts, uint16_t* cc2_counts); +void ob2_display(const Ob2Ui& ui); diff --git a/firmware2/OpenBeacon2/ob2_hold.cpp b/firmware2/OpenBeacon2/ob2_hold.cpp new file mode 100644 index 00000000..77f5025e --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_hold.cpp @@ -0,0 +1,30 @@ +#include "ob2_hold.h" + +void ob2_hold_init(Ob2Hold* hold) { + hold->stable = 0; + hold->pending = 0; + hold->since_ms = 0; + hold->pending_valid = false; + hold->stable_valid = false; +} + +bool ob2_hold_feed(Ob2Hold* hold, uint8_t sample, uint32_t now_ms, uint32_t hold_ms) { + if (hold->stable_valid && sample == hold->stable) { + hold->pending_valid = false; + return false; + } + if (!hold->pending_valid || sample != hold->pending) { + hold->pending = sample; + hold->since_ms = now_ms; + hold->pending_valid = true; + return false; + } + if ((now_ms - hold->since_ms) < hold_ms) { + return false; + } + const bool changed = !hold->stable_valid || hold->stable != sample; + hold->stable = sample; + hold->stable_valid = true; + hold->pending_valid = false; + return changed; +} diff --git a/firmware2/OpenBeacon2/ob2_hold.h b/firmware2/OpenBeacon2/ob2_hold.h new file mode 100644 index 00000000..0659819a --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_hold.h @@ -0,0 +1,20 @@ +#pragma once + +/* Non-blocking "sample must hold for N ms" used by buttons and CC. */ + +#include + +constexpr uint16_t OB2_BUTTON_DEBOUNCE_MS = 25; + +struct Ob2Hold { + uint8_t stable; + uint8_t pending; + uint32_t since_ms; + bool pending_valid; + bool stable_valid; +}; + +void ob2_hold_init(Ob2Hold* hold); + +/* True when the stable value changes, including the first time it locks. */ +bool ob2_hold_feed(Ob2Hold* hold, uint8_t sample, uint32_t now_ms, uint32_t hold_ms); diff --git a/firmware2/OpenBeacon2/ob2_power.cpp b/firmware2/OpenBeacon2/ob2_power.cpp new file mode 100644 index 00000000..b6dbb46f --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_power.cpp @@ -0,0 +1,45 @@ +#include "ob2_power.h" + +uint16_t ob2_adc_counts_to_mv(uint16_t counts) { + if (counts > 4095) { + counts = 4095; + } + return static_cast((static_cast(counts) * OB2_ADC_VREF_MV) / + OB2_ADC_FULL_SCALE); +} + +Ob2CcLevel ob2_cc_classify_mv(uint16_t cc1_mv, uint16_t cc2_mv) { + if (cc1_mv < OB2_CC_ABSENT_MV && cc2_mv < OB2_CC_ABSENT_MV) { + return Ob2CcLevel::None; + } + const uint16_t higher = cc1_mv > cc2_mv ? cc1_mv : cc2_mv; + if (higher >= OB2_CC_3A_MV) { + return Ob2CcLevel::A3000; + } + if (higher >= OB2_CC_1A5_MV) { + return Ob2CcLevel::A1500; + } + return Ob2CcLevel::Default; +} + +uint16_t ob2_isense_ma(uint16_t sense_mv) { + return static_cast(sense_mv / OB2_ISENSE_MV_PER_MA); +} + +bool ob2_isense_specified(uint16_t ma_value) { + return ma_value >= OB2_ISENSE_UNSPEC_MA; +} + +const char* ob2_cc_name(Ob2CcLevel level) { + switch (level) { + case Ob2CcLevel::None: + return "none"; + case Ob2CcLevel::Default: + return "default"; + case Ob2CcLevel::A1500: + return "1.5A"; + case Ob2CcLevel::A3000: + return "3A"; + } + return "?"; +} diff --git a/firmware2/OpenBeacon2/ob2_power.h b/firmware2/OpenBeacon2/ob2_power.h new file mode 100644 index 00000000..6703fb41 --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_power.h @@ -0,0 +1,32 @@ +#pragma once + +/* + * ADC math for section 4.14. The converter is 12-bit with the internal + * 2.5 V reference. VREFA is never used (PD7 is a button). + * + * CC: use the higher of CC1 and CC2. Both under 0.2 V means no source, + * which policy treats as Default. Under 0.66 V is Default, 0.66 V up to + * but not including 1.23 V is 1.5 A, and 1.23 V and above is 3 A. + * + * PA current is V(TX_ISENSE) / 4.0 V/A. Readings under 100 mA are unspecified. + * isense_k is a later calibration; this milestone uses the nominal scale. + */ + +#include + +enum class Ob2CcLevel : uint8_t { None = 0, Default, A1500, A3000 }; + +constexpr uint16_t OB2_ADC_VREF_MV = 2500; +constexpr uint16_t OB2_ADC_FULL_SCALE = 4096; +constexpr uint16_t OB2_CC_ABSENT_MV = 200; +constexpr uint16_t OB2_CC_1A5_MV = 660; +constexpr uint16_t OB2_CC_3A_MV = 1230; +constexpr uint16_t OB2_CC_DEBOUNCE_MS = 200; +constexpr uint16_t OB2_ISENSE_UNSPEC_MA = 100; +constexpr uint16_t OB2_ISENSE_MV_PER_MA = 4; + +uint16_t ob2_adc_counts_to_mv(uint16_t counts); +Ob2CcLevel ob2_cc_classify_mv(uint16_t cc1_mv, uint16_t cc2_mv); +uint16_t ob2_isense_ma(uint16_t sense_mv); +bool ob2_isense_specified(uint16_t ma_value); +const char* ob2_cc_name(Ob2CcLevel level); diff --git a/firmware2/OpenBeacon2/ob2_pwm.h b/firmware2/OpenBeacon2/ob2_pwm.h new file mode 100644 index 00000000..298a3300 --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_pwm.h @@ -0,0 +1,52 @@ +#pragma once + +/* + * K3, checked against Microchip DS40002183E (AVR128DA28/32/48/64). + * + * 16.3.7 TCAROUTEA (p.161): TCA0 PORTA is PA0-PA5, so PA5 is WO5. + * TCA0 PORTB is PB0-PB5, so PB1 is WO1. One PORTMUX value. + * TCA1 PORTB is PB0-PB5. TCA1 has no PORTA route. + * 16.3.8 TCBROUTEA (p.162): TCB0 PA2/PF4, TCB1 PA3/PF5, TCB2 PC0/PB4, + * TCB3 PB5/PC1, TCB4 PG3/PC6. No TCB waveform on PA5, PA6, or PB1. + * 16.3.9 TCDROUTEA (p.163): default WOA PA4, WOB PA5, WOC PA6, WOD PA7. + * 23.5.3 TCD.CTRLC (p.321): CMPCSEL reset 0 connects WOC to PWMA (WOA). + * A bench PWM build must set CMPCSEL to PWMB or PA6 follows PA4. + * WOC then has the same duty as WOB. + * TCD.FAULTCTRL CMPAEN must stay off. WOA is PA4, the TX key. + * I/O mux table: PA6 has TCD WOC and no TCA waveform output. + * + * TCA1 WO0 is PB0 (GPS USART3 TX). Leave that compare output disabled. + * Shipped firmware does not PWM the relays. Do not analogWrite PA4-PA7: + * DxCore can route that range to TCD0, which would drive the TX key. + */ + +#include + +constexpr bool OB2_RELAY_PWM_SHIPPED = false; +constexpr bool OB2_TCA0_SERVES_PA5_AND_PB1 = false; +constexpr bool OB2_TCB_DRIVES_RELAY_PINS = false; + +struct Ob2TimerPin { + char port; + int8_t bit; + const char* timer; + const char* signal; + const char* portmux; + bool bench_candidate; + const char* note; +}; + +static constexpr Ob2TimerPin OB2_TIMER_PINS[] = { + {'A', 5, "TCA0", "WO5", "PORTA", true, "TCA0 on PORTA. Not available if TCA0 moves to PORTB."}, + {'A', 5, "TCD0", "WOB", "DEFAULT", true, "Proposed bench output with PA6."}, + {'A', 6, "TCD0", "WOC", "DEFAULT", true, + "Only timer output on PA6. Reset CMPCSEL follows WOA (PA4); set CMPCSEL to PWMB."}, + {'B', 1, "TCA1", "WO1", "PORTB", true, "Proposed bench output. Keep TCA1 WO0 (PB0) disabled."}, + {'B', 1, "TCA0", "WO1", "PORTB", true, "Uses the only TCA0 route, so PA5 WO5 is gone."}, + {'A', 4, "TCD0", "WOA", "DEFAULT", false, "TX key and TX LED. Enabling WOA keys the PA supply."}, + {'B', 0, "TCA1", "WO0", "PORTB", false, "USART3 TX to the GPS."}, + {'B', 0, "TCA0", "WO0", "PORTB", false, "Same pin when TCA0 is routed to PORTB."}, +}; + +constexpr uint8_t OB2_TIMER_PIN_COUNT = + sizeof(OB2_TIMER_PINS) / sizeof(OB2_TIMER_PINS[0]); diff --git a/firmware2/OpenBeacon2/ob2_relays.cpp b/firmware2/OpenBeacon2/ob2_relays.cpp new file mode 100644 index 00000000..44e273ae --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_relays.cpp @@ -0,0 +1,129 @@ +#include "ob2_relays.h" + +bool ob2_relay_legal(uint8_t a, uint8_t b, uint8_t c) { + if (a > 1 || b > 1 || c > 1) { + return false; + } + if (c == 1 && a == 0) { + return false; /* 001 and 011 */ + } + if (b == 1 && a == 1) { + return false; /* 110 and 111, the x11 codes with A on */ + } + if (b == 1 && c == 1) { + return false; + } + return true; +} + +Ob2Relay ob2_relay_from_abc(uint8_t a, uint8_t b, uint8_t c) { + Ob2Relay r = {0, 0, 0, 0, 0, false}; + if (!ob2_relay_legal(a, b, c)) { + return r; + } + r.a = a; + r.b = b; + r.c = c; + r.legal = true; + if (a == 0 && b == 0 && c == 0) { + r.coils = 0; + r.load_ma = 0; + } else if (c == 1) { + r.coils = 4; /* K1 K4 K3 K6 */ + r.load_ma = 84; + } else { + r.coils = 2; + r.load_ma = 42; + } + return r; +} + +Ob2Relay ob2_relay_for_filter(Ob2Filter filter) { + switch (filter) { + case Ob2Filter::F2: + return ob2_relay_from_abc(0, 0, 0); + case Ob2Filter::F3: + return ob2_relay_from_abc(0, 1, 0); + case Ob2Filter::F4: + return ob2_relay_from_abc(1, 0, 0); + case Ob2Filter::F5: + return ob2_relay_from_abc(1, 0, 1); + } + return ob2_relay_from_abc(2, 0, 0); +} + +bool ob2_filter_for_band(Ob2Band band, Ob2Filter* out) { + Ob2Filter filter = Ob2Filter::F2; + switch (band) { + case Ob2Band::M80: + case Ob2Band::M60: + filter = Ob2Filter::F2; + break; + case Ob2Band::M40: + case Ob2Band::M30: + filter = Ob2Filter::F3; + break; + case Ob2Band::M20: + case Ob2Band::M17: + filter = Ob2Filter::F4; + break; + case Ob2Band::M15: + case Ob2Band::M12: + case Ob2Band::M10: + filter = Ob2Filter::F5; + break; + case Ob2Band::M2200: + case Ob2Band::M630: + case Ob2Band::M160: + case Ob2Band::M6: + case Ob2Band::M2: + return false; + } + if (out != nullptr) { + *out = filter; + } + return true; +} + +const char* ob2_filter_name(Ob2Filter filter) { + switch (filter) { + case Ob2Filter::F2: + return "F2"; + case Ob2Filter::F3: + return "F3"; + case Ob2Filter::F4: + return "F4"; + case Ob2Filter::F5: + return "F5"; + } + return "?"; +} + +const char* ob2_filter_bands(Ob2Filter filter) { + switch (filter) { + case Ob2Filter::F2: + return "80/60m"; + case Ob2Filter::F3: + return "40/30m"; + case Ob2Filter::F4: + return "20/17m"; + case Ob2Filter::F5: + return "15/12/10m"; + } + return "?"; +} + +uint32_t ob2_filter_carrier_hz(Ob2Filter filter) { + /* WSPR dial frequencies. Each one sits in that filter's band list. */ + switch (filter) { + case Ob2Filter::F2: + return 3568600UL; + case Ob2Filter::F3: + return 7038600UL; + case Ob2Filter::F4: + return 14095600UL; + case Ob2Filter::F5: + return 21094600UL; + } + return 0; +} diff --git a/firmware2/OpenBeacon2/ob2_relays.h b/firmware2/OpenBeacon2/ob2_relays.h new file mode 100644 index 00000000..3856d337 --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_relays.h @@ -0,0 +1,54 @@ +#pragma once + +/* + * Section 2.2 filter codes. Only 000, 010, 100, and 101 are legal. + * 001, 011, 110, and 111 are never driven. At most four coils. + * Release firmware drives these as GPIO, fully on. See ob2_pwm.h. + */ + +#include + +enum class Ob2Filter : uint8_t { F2 = 0, F3 = 1, F4 = 2, F5 = 3 }; + +enum class Ob2Band : uint8_t { + M2200 = 0, + M630, + M160, + M80, + M60, + M40, + M30, + M20, + M17, + M15, + M12, + M10, + M6, + M2 +}; + +constexpr uint8_t OB2_BAND_COUNT = 14; +constexpr uint8_t OB2_FILTER_COUNT = 4; +constexpr uint8_t OB2_RELAY_SETTLE_MS = 10; + +struct Ob2Relay { + uint8_t a; + uint8_t b; + uint8_t c; + uint8_t coils; + uint16_t load_ma; + bool legal; +}; + +bool ob2_relay_legal(uint8_t a, uint8_t b, uint8_t c); +Ob2Relay ob2_relay_from_abc(uint8_t a, uint8_t b, uint8_t c); +Ob2Relay ob2_relay_for_filter(Ob2Filter filter); + +/* False when the band has no filter. Transmit is refused in that case. */ +bool ob2_filter_for_band(Ob2Band band, Ob2Filter* out); + +const char* ob2_filter_name(Ob2Filter filter); +const char* ob2_filter_bands(Ob2Filter filter); + +/* Fixed bring-up carrier, inside that filter's band list. Hertz. */ +uint32_t ob2_filter_carrier_hz(Ob2Filter filter); diff --git a/firmware2/OpenBeacon2/ob2_softstart.cpp b/firmware2/OpenBeacon2/ob2_softstart.cpp new file mode 100644 index 00000000..4ae272b2 --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_softstart.cpp @@ -0,0 +1,12 @@ +#include "ob2_softstart.h" + +bool ob2_may_enable_boost(uint32_t now_ms, bool cc_sampled) { + return cc_sampled && now_ms >= OB2_BOOT_KEY_DELAY_MS; +} + +bool ob2_may_ramp_bias(bool boost_on, uint32_t now_ms, uint32_t boost_on_ms) { + if (!boost_on) { + return false; + } + return (now_ms - boost_on_ms) >= OB2_PA_SETTLE_MS; +} diff --git a/firmware2/OpenBeacon2/ob2_softstart.h b/firmware2/OpenBeacon2/ob2_softstart.h new file mode 100644 index 00000000..6023bc1b --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_softstart.h @@ -0,0 +1,16 @@ +#pragma once + +/* + * Section 4.14 soft-start. No keying until 50 ms after power-up and the + * first CC sample. After PA4 turns the boost on, wait at least 20 ms + * before the bias ramp. Times are milliseconds from the same clock the + * caller uses (millis() on the AVR). + */ + +#include + +constexpr uint32_t OB2_BOOT_KEY_DELAY_MS = 50; +constexpr uint32_t OB2_PA_SETTLE_MS = 20; + +bool ob2_may_enable_boost(uint32_t now_ms, bool cc_sampled); +bool ob2_may_ramp_bias(bool boost_on, uint32_t now_ms, uint32_t boost_on_ms); diff --git a/firmware2/OpenBeacon2/ob2_ui.h b/firmware2/OpenBeacon2/ob2_ui.h new file mode 100644 index 00000000..14481292 --- /dev/null +++ b/firmware2/OpenBeacon2/ob2_ui.h @@ -0,0 +1,42 @@ +#pragma once + +#include "ob2_power.h" + +#include + +enum Ob2Button : uint8_t { + Ob2BtnUp = 0, + Ob2BtnDown, + Ob2BtnLeft, + Ob2BtnRight, + Ob2BtnBack, + Ob2BtnDsp1, + Ob2BtnDsp2, + Ob2BtnCount +}; + +struct Ob2Ui { + const char* filter_name; + const char* filter_bands; + uint8_t a; + uint8_t b; + uint8_t c; + uint8_t coils; + uint16_t load_ma; + Ob2CcLevel cc_raw; + Ob2CcLevel cc_stable; + bool cc_stable_valid; + uint16_t cc1_mv; + uint16_t cc2_mv; + uint16_t isense_ma; + bool isense_spec; + bool may_key; + bool boost; + bool bias_live; + bool synth_ok; + bool display_ok; + bool eeprom_ok; + bool eeprom_busy; + uint8_t buttons; + uint32_t carrier_hz; +}; diff --git a/firmware2/README.md b/firmware2/README.md index db5de4bc..43b74f1e 100644 --- a/firmware2/README.md +++ b/firmware2/README.md @@ -1,12 +1,12 @@ # firmware2 -Clean-sheet firmware for the OpenBeacon 2 Rev C (AVR128DA48, DxCore). Milestone M0 is the tree, the CI build, a host test, and the U9 pin map. Feature code starts at M1. +Clean-sheet firmware for the OpenBeacon 2 Rev C (AVR128DA48, DxCore). Milestone M1 is the hardware bring-up: Optiboot notes, OLED, buttons, the SYNC LED, 3-bit relays, the current and CC ADCs, soft-start waits, a fixed Si5351 CLK0 carrier, the MCP4725, and a 24AA64 presence check. The SAMD21 firmware under `firmware/` is the capability guide only. Nothing here is ported from it. ## Toolchain -CI builds with [arduino-cli](https://arduino.github.io/arduino-cli/) and DxCore, which is what spec section 4.1 specifies, and what section 4.11 uses for Optiboot uploads over the CP2102N. PlatformIO can drive the same core, but the board options this hardware needs are DxCore menu ids (chip, clock, millis timer, bootloader USART). arduino-cli passes those as the FQBN, so CI does not depend on a PlatformIO platform package tracking DxCore. +CI builds with [arduino-cli](https://arduino.github.io/arduino-cli/) and DxCore, which is what spec section 4.1 specifies, and what section 4.11 uses for Optiboot uploads over the CP2102N. PlatformIO can drive the same core, but the board options this hardware needs are DxCore menu ids (chip, clock, millis timer, bootloader USART). arduino-cli passes those as the FQBN. The FQBN is: @@ -16,47 +16,97 @@ DxCore:megaavr:avrdaopti:chip=avr128da48,clock=24internal,millis=tcb2,bootloader | Option | Why | |---|---| -| `avrdaopti` / AVR128DA48 | Optiboot build. Flash limit is 130560 bytes (512 bytes reserved), matching section 4.11. M0 does not burn the bootloader. | +| `avrdaopti` / AVR128DA48 | Optiboot build. Flash limit is 130560 bytes (512 bytes reserved). | | `clock=24internal` | 24 MHz internal OSCHF. See the autotune note below. | -| `millis=tcb2` | Spec section 4.15 keeps `millis` on TCB2 so TCB0/TCB1 stay free for the CLK4 counter. | -| `bootloaderusart=ser0` | USART0 default pins PA0/PA1, the CP2102N port. That menu entry is internal-clock only, which matches the clock option. | +| `millis=tcb2` | Section 4.15 keeps `millis` on TCB2 so TCB0 and TCB1 stay free. | +| `bootloaderusart=ser0` | USART0 on PA0/PA1, the CP2102N port. | -Pinned versions: arduino-cli 1.5.1, DxCore 1.6.2 (the current board-manager release). The core is installed from Spence Konde's GitHub copy of `package_drazzy.com_index.json`: +Pinned versions: arduino-cli 1.5.1, DxCore 1.6.2, Etherkit Si5351 2.4.0, U8g2 2.36.19. The core index is Spence Konde's GitHub copy of `package_drazzy.com_index.json`: `https://raw.githubusercontent.com/SpenceKonde/ReleaseScripts/master/package_drazzy.com_index.json` -`drazzy.com`'s TLS certificate expired on 20 Oct 2026, so the canonical board-manager URL fails verification. The GitHub copy is the same index and its platform and toolchain URLs are on `github.io` and S3. +`drazzy.com`'s TLS certificate expired on 20 Oct 2026, so the canonical board-manager URL fails verification. ### OSCHF and the 32.768 kHz crystal -`clock=24internal` makes DxCore write `CLKCTRL.OSCHFCTRLA` frequency select for 24 MHz (`0x09 << 2` in `wiring.c`). It does not set the autotune bit, and DxCore has no menu for "OSCHF auto-tuned to XOSC32K". Spec section 4.1 describes that as the intended clock. M0 does not turn autotune on in the stub: it needs the crystal running, and it belongs with the timebase. The stub is 24 MHz internal, not crystal-disciplined. +`clock=24internal` makes DxCore write `CLKCTRL.OSCHFCTRLA` frequency select for 24 MHz. It does not set the autotune bit, and DxCore has no menu for "OSCHF auto-tuned to XOSC32K". Section 4.1 describes that as the intended clock. M1 still does not turn autotune on. The sketch runs at 24 MHz internal, not crystal-disciplined. ## Layout ``` firmware2/ - OpenBeacon2/ sketch (Arduino builder compiles this directory) - OpenBeacon2.ino M0 stub: relay drivers and SYNC LED held off - ob2_pins.h U9 pin map, no Arduino headers - ob2_pin_check.cpp static_asserts against the DxCore pin macros - host/ CMake + doctest, Linux - third_party/doctest/ doctest 2.4.11 (its own MIT license, not this project's) - tools/build.sh install DxCore if needed, then compile + OpenBeacon2/ sketch (the Arduino builder compiles this directory) + OpenBeacon2.ino bring-up loop + ob2_pins.h U9 pin map + ob2_relays.* section 2.2 codes (host-tested) + ob2_power.* ADC millivolts, CC class, isense + ob2_softstart.* 50 ms boot gate and 20 ms boost settle + ob2_dac.* MCP4725 code + ob2_pwm.h DS40002183E timer check; PWM is not enabled + ob2_hal.* GPIO, ADC, I2C devices + ob2_display.cpp SH1106 on SPI1 ALT1 + host/ CMake + doctest + third_party/doctest/ doctest 2.4.11 (its own MIT license) + tools/build.sh install the core and libraries, then compile ``` -Later logic modules stay free of Arduino headers so the host tests can build them. M0 has no logic modules yet. The sample test locks the pin table. +Logic modules that the host tests build do not include Arduino headers. -## Pin map +## What the sketch does -`ob2_pins.h` follows spec section 2.1, with section 2.2 applied: +Relays start at F2 (all coils off) and are plain GPIO. UP and DOWN cycle F2, F3, F4, F5. A filter change while the carrier is on turns the carrier off first. DSP1 (Menu1) is the only way to key: PA4 goes high after 50 ms from reset and a CC sample, then the sketch waits 20 ms before writing 500 mV to the MCP4725 and enabling CLK0. BACK or DSP2 turns bias off, disables CLK0, then drops PA4. The SYNC LED blinks and stays on while any button is held. The OLED is a SH1106 page buffer on SPI1 ALT1. Nothing calls `delay()`. U8g2's own reset pulse does. -* PE1 `TX_ISENSE`, PE2 `CC1_SENSE`, PE3 `CC2_SENSE` -* PB1 `BAND_C` (USART3 RXEN stays off) -* GPS RX on USART2 PF5, ALT1, RX only (PF4 stays GPIO) -* MCU TX to the GPS on USART3 PB0 -* CLK4 cal input on PB2 +CLK0 frequencies, one per filter, are the WSPR dials 3568600, 7038600, 14095600, and 21094600 Hz. CLK2 and CLK4 stay off. Drive is 8 mA because the old firmware used that; section 4.4 does not restate it. The 500 mV bias is a bench placeholder, not a calibration. -The power-rework schematic is not in this repository. Commit `88e9a62` on `pcb-layout` still has PB1 unconnected and PE1-PE3 on J5. The header says so, in `OB2_POWER_REWORK_NOTE` and on those four rows. +Power-rework hardware (PB1 `BAND_C`, PE1 `TX_ISENSE`, PE2 `CC1_SENSE`, PE3 `CC2_SENSE`) is still not in this repository. `88e9a62` on `pcb-layout` has PB1 unconnected and PE1-PE3 on J5. + +## Bench 1 — Optiboot + +Programmer: DxCore `serialupdi` (230400 baud) on the UPDI dongle attached to J13. Upload after that is the CP2102N, 115200, DTR through C37/R36. + +DxCore 1.6.2 resolves this FQBN to entry condition `_extr` and this fuse image: + +| Fuse | Value | Meaning | +|---|---|---| +| 0 WDTCFG | `0x00` | Watchdog off | +| 1 BODCFG | `0x00` | BOD off (menu default) | +| 2 OSCCFG | `0x00` | Internal HF oscillator | +| 5 SYSCFG0 | `0xC0` | What `burn-bootloader` writes. See below. | +| 6 SYSCFG1 | `0x04` | 8 ms startup | +| 7 CODESIZE | `0x00` | | +| 8 BOOTSIZE | `0x01` | 512-byte boot section | + +Two DxCore 1.6.2 holes, from `arduino-cli compile --show-properties` on this FQBN: + +1. `bootloader.class` is unset. The burn pattern looks for `{bootloader.class}_ser0_extr.hex`, which is not a file. The image that matches the 128 kB DA part, USART0 default pins, and `_extr` is `optiboot_128dx_ser0_extr.hex` in the DxCore `bootloaders/hex` directory. +2. Both Reset-pin menu choices set `resetpinbit=0`, so SYSCFG0 is `0b11000000` (`0xC0`). DS40002183E section 7.9.2.4: RSTPINCFG `00` makes PF6 a GPIO, and `0x2` enables external reset. DTR will not enter the bootloader until fuse 5 is `0xC8` (`0b11001000`: same CRC fields, RSTPINCFG = RESET, EESAVE still clear). The non-Optiboot DA menu does set the bit. This FQBN cannot. + +EESAVE is clear, and the burn does a chip erase, so internal EEPROM is wiped. The 24AA64 is not part of that erase. + +From the DxCore package directory, with `PROG` the SerialUPDI port and `APP` the CP2102N port: + +```bash +FQBN='DxCore:megaavr:avrdaopti:chip=avr128da48,clock=24internal,millis=tcb2,bootloaderusart=ser0' +HEX="$HOME/.arduino15/packages/DxCore/hardware/megaavr/1.6.2/bootloaders/hex/optiboot_128dx_ser0_extr.hex" +PY="$HOME/.arduino15/packages/DxCore/hardware/megaavr/1.6.2/tools/prog.py" + +# Fuse 5 is 0xC8 so PF6 is reset. 0xC0 is what burn-bootloader would have written. +python3 "$PY" -t uart -u "$PROG" -b 230400 -d avr128da48 \ + --fuses 0:0x00 1:0x00 2:0x00 5:0xC8 6:0x04 7:0x00 8:0x01 \ + -f "$HEX" -a write + +arduino-cli upload -b "$FQBN" -p "$APP" firmware2/OpenBeacon2 +``` + +Use a dummy load. DSP1 transmits. Recovery of a wiped bootloader is the same `prog.py` write over J13. + +## Bench 2 — panel and relays + +1. OLED shows `OB2 F2 80/60m`, `ABC 000`, `EE ok` after the 24AA64 answers, and `key locked` for the first 50 ms (it will already say `key idle` if the display init took longer than that). +2. Hold each button. Its letter in `btn UDLRB12` lights, and the SYNC LED stays on for the hold. Released, the LED blinks. +3. UP/DOWN steps F2 → F3 → F4 → F5 and back. Scope the three gates: `000`, `010`, `100`, `101` only. Coil current on the +5 V relay rail should step 0 / 42 / 42 / 84 mA, and never more than four coils. PA4 stays low during this. +4. DSP1 on F2: PA4 rises, and about 20 ms later CLK0 is 3.5686 MHz and the bias pin leaves 0 V (500 mV command into the MCP4725). BACK or DSP2 returns bias to 0, kills CLK0, then drops PA4. Repeat on F4 and confirm 14.0956 MHz, and that the relays changed only while the carrier was off. +5. CC and TX_ISENSE volts are on the OLED (`CC` line, `I` line). Under 100 mA the current line stays `<100`. No overcurrent trip in this milestone. ## Build @@ -69,4 +119,4 @@ cmake --build firmware2/host/build ctest --test-dir firmware2/host/build --output-on-failure ``` -CI is `.github/workflows/firmware2.yml`. It compiles the sketch and runs the host test. +CI is `.github/workflows/firmware2.yml`. The host job uses the image's `cmake` and `g++` (no apt install). diff --git a/firmware2/host/CMakeLists.txt b/firmware2/host/CMakeLists.txt index 34b45999..7d42ebe7 100644 --- a/firmware2/host/CMakeLists.txt +++ b/firmware2/host/CMakeLists.txt @@ -17,11 +17,18 @@ set(CMAKE_CXX_EXTENSIONS OFF) enable_testing() -add_executable(ob2_host_tests test_pins.cpp) +add_executable(ob2_host_tests + test_pins.cpp + test_hal.cpp + ../OpenBeacon2/ob2_relays.cpp + ../OpenBeacon2/ob2_power.cpp + ../OpenBeacon2/ob2_softstart.cpp + ../OpenBeacon2/ob2_dac.cpp + ../OpenBeacon2/ob2_hold.cpp) target_include_directories(ob2_host_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../OpenBeacon2) target_include_directories(ob2_host_tests SYSTEM PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../third_party/doctest) target_compile_options(ob2_host_tests PRIVATE -Wall -Wextra -Werror) -add_test(NAME pins COMMAND ob2_host_tests) +add_test(NAME host COMMAND ob2_host_tests) diff --git a/firmware2/host/test_hal.cpp b/firmware2/host/test_hal.cpp new file mode 100644 index 00000000..b41d8399 --- /dev/null +++ b/firmware2/host/test_hal.cpp @@ -0,0 +1,279 @@ +#include "doctest.h" + +#include "ob2_bus.h" +#include "ob2_dac.h" +#include "ob2_hold.h" +#include "ob2_pins.h" +#include "ob2_power.h" +#include "ob2_pwm.h" +#include "ob2_relays.h" +#include "ob2_softstart.h" + +#include + +TEST_CASE("only four relay codes are legal and none exceed four coils") { + const uint8_t legal[4][3] = {{0, 0, 0}, {0, 1, 0}, {1, 0, 0}, {1, 0, 1}}; + const uint8_t coils[4] = {0, 2, 2, 4}; + const uint16_t load[4] = {0, 42, 42, 84}; + for (int i = 0; i < 4; ++i) { + CHECK(ob2_relay_legal(legal[i][0], legal[i][1], legal[i][2])); + const Ob2Relay r = ob2_relay_from_abc(legal[i][0], legal[i][1], legal[i][2]); + CHECK(r.legal); + CHECK(r.coils == coils[i]); + CHECK(r.load_ma == load[i]); + CHECK(r.coils <= 4); + } + + const uint8_t illegal[4][3] = {{0, 0, 1}, {0, 1, 1}, {1, 1, 0}, {1, 1, 1}}; + for (int i = 0; i < 4; ++i) { + CHECK_FALSE(ob2_relay_legal(illegal[i][0], illegal[i][1], illegal[i][2])); + const Ob2Relay r = ob2_relay_from_abc(illegal[i][0], illegal[i][1], illegal[i][2]); + CHECK_FALSE(r.legal); + CHECK(r.a == 0); + CHECK(r.b == 0); + CHECK(r.c == 0); + CHECK(r.coils == 0); + } + CHECK_FALSE(ob2_relay_legal(2, 0, 0)); +} + +TEST_CASE("filters map to the section 2.2 table") { + const Ob2Relay f2 = ob2_relay_for_filter(Ob2Filter::F2); + const Ob2Relay f3 = ob2_relay_for_filter(Ob2Filter::F3); + const Ob2Relay f4 = ob2_relay_for_filter(Ob2Filter::F4); + const Ob2Relay f5 = ob2_relay_for_filter(Ob2Filter::F5); + CHECK(f2.a == 0); + CHECK(f2.b == 0); + CHECK(f2.c == 0); + CHECK(f3.a == 0); + CHECK(f3.b == 1); + CHECK(f3.c == 0); + CHECK(f4.a == 1); + CHECK(f4.b == 0); + CHECK(f4.c == 0); + CHECK(f5.a == 1); + CHECK(f5.b == 0); + CHECK(f5.c == 1); +} + +TEST_CASE("every band lands on its filter or is refused") { + struct Row { + Ob2Band band; + bool has; + Ob2Filter filter; + }; + const Row rows[] = { + {Ob2Band::M2200, false, Ob2Filter::F2}, {Ob2Band::M630, false, Ob2Filter::F2}, + {Ob2Band::M160, false, Ob2Filter::F2}, {Ob2Band::M6, false, Ob2Filter::F2}, + {Ob2Band::M2, false, Ob2Filter::F2}, {Ob2Band::M80, true, Ob2Filter::F2}, + {Ob2Band::M60, true, Ob2Filter::F2}, {Ob2Band::M40, true, Ob2Filter::F3}, + {Ob2Band::M30, true, Ob2Filter::F3}, {Ob2Band::M20, true, Ob2Filter::F4}, + {Ob2Band::M17, true, Ob2Filter::F4}, {Ob2Band::M15, true, Ob2Filter::F5}, + {Ob2Band::M12, true, Ob2Filter::F5}, {Ob2Band::M10, true, Ob2Filter::F5}, + }; + CHECK(sizeof(rows) / sizeof(rows[0]) == OB2_BAND_COUNT); + for (const Row& row : rows) { + Ob2Filter got = Ob2Filter::F2; + CHECK(ob2_filter_for_band(row.band, &got) == row.has); + if (row.has) { + CHECK(got == row.filter); + } + } +} + +TEST_CASE("fixed carriers are the WSPR dials inside each filter") { + CHECK(ob2_filter_carrier_hz(Ob2Filter::F2) == 3568600UL); + CHECK(ob2_filter_carrier_hz(Ob2Filter::F3) == 7038600UL); + CHECK(ob2_filter_carrier_hz(Ob2Filter::F4) == 14095600UL); + CHECK(ob2_filter_carrier_hz(Ob2Filter::F5) == 21094600UL); + CHECK(std::strcmp(ob2_filter_bands(Ob2Filter::F2), "80/60m") == 0); + CHECK(std::strcmp(ob2_filter_bands(Ob2Filter::F5), "15/12/10m") == 0); +} + +TEST_CASE("soft-start refuses keying before 50 ms or the first CC sample") { + CHECK_FALSE(ob2_may_enable_boost(0, true)); + CHECK_FALSE(ob2_may_enable_boost(49, true)); + CHECK_FALSE(ob2_may_enable_boost(50, false)); + CHECK_FALSE(ob2_may_enable_boost(1000, false)); + CHECK(ob2_may_enable_boost(50, true)); + CHECK(ob2_may_enable_boost(51, true)); +} + +TEST_CASE("bias ramp waits 20 ms after the boost is enabled") { + CHECK_FALSE(ob2_may_ramp_bias(false, 100, 0)); + CHECK_FALSE(ob2_may_ramp_bias(true, 0, 0)); + CHECK_FALSE(ob2_may_ramp_bias(true, 19, 0)); + CHECK(ob2_may_ramp_bias(true, 20, 0)); + CHECK(ob2_may_ramp_bias(true, 70, 50)); + CHECK_FALSE(ob2_may_ramp_bias(true, 69, 50)); + CHECK(ob2_may_ramp_bias(true, 10, 0xFFFFFFF0UL)); +} + +TEST_CASE("ADC counts use a 2.5 V 12-bit scale") { + CHECK(ob2_adc_counts_to_mv(0) == 0); + CHECK(ob2_adc_counts_to_mv(1) == 0); + CHECK(ob2_adc_counts_to_mv(2) == 1); + CHECK(ob2_adc_counts_to_mv(4095) == 2499); + CHECK(ob2_adc_counts_to_mv(4096) == 2499); +} + +TEST_CASE("CC thresholds use the higher pin and both orientations") { + CHECK(ob2_cc_classify_mv(0, 0) == Ob2CcLevel::None); + CHECK(ob2_cc_classify_mv(199, 199) == Ob2CcLevel::None); + CHECK(ob2_cc_classify_mv(200, 0) == Ob2CcLevel::Default); + CHECK(ob2_cc_classify_mv(0, 200) == Ob2CcLevel::Default); + CHECK(ob2_cc_classify_mv(659, 10) == Ob2CcLevel::Default); + CHECK(ob2_cc_classify_mv(10, 659) == Ob2CcLevel::Default); + CHECK(ob2_cc_classify_mv(660, 0) == Ob2CcLevel::A1500); + CHECK(ob2_cc_classify_mv(0, 660) == Ob2CcLevel::A1500); + CHECK(ob2_cc_classify_mv(1229, 100) == Ob2CcLevel::A1500); + CHECK(ob2_cc_classify_mv(100, 1229) == Ob2CcLevel::A1500); + CHECK(ob2_cc_classify_mv(1230, 0) == Ob2CcLevel::A3000); + CHECK(ob2_cc_classify_mv(0, 1230) == Ob2CcLevel::A3000); + CHECK(ob2_cc_classify_mv(2500, 100) == Ob2CcLevel::A3000); + CHECK(std::strcmp(ob2_cc_name(Ob2CcLevel::None), "none") == 0); + CHECK(std::strcmp(ob2_cc_name(Ob2CcLevel::A1500), "1.5A") == 0); +} + +TEST_CASE("CC debounce holds a new level for 200 ms") { + Ob2Hold hold; + ob2_hold_init(&hold); + CHECK_FALSE(ob2_hold_feed(&hold, static_cast(Ob2CcLevel::Default), 0, OB2_CC_DEBOUNCE_MS)); + CHECK_FALSE(hold.stable_valid); + CHECK_FALSE(ob2_hold_feed(&hold, static_cast(Ob2CcLevel::Default), 199, OB2_CC_DEBOUNCE_MS)); + CHECK_FALSE(hold.stable_valid); + CHECK(ob2_hold_feed(&hold, static_cast(Ob2CcLevel::Default), 200, OB2_CC_DEBOUNCE_MS)); + CHECK(hold.stable == static_cast(Ob2CcLevel::Default)); + + CHECK_FALSE(ob2_hold_feed(&hold, static_cast(Ob2CcLevel::A1500), 200, OB2_CC_DEBOUNCE_MS)); + CHECK(hold.stable == static_cast(Ob2CcLevel::Default)); + CHECK_FALSE(ob2_hold_feed(&hold, static_cast(Ob2CcLevel::A1500), 399, OB2_CC_DEBOUNCE_MS)); + CHECK(ob2_hold_feed(&hold, static_cast(Ob2CcLevel::A1500), 400, OB2_CC_DEBOUNCE_MS)); + CHECK(hold.stable == static_cast(Ob2CcLevel::A1500)); + + CHECK_FALSE(ob2_hold_feed(&hold, static_cast(Ob2CcLevel::A3000), 400, OB2_CC_DEBOUNCE_MS)); + CHECK_FALSE(ob2_hold_feed(&hold, static_cast(Ob2CcLevel::Default), 500, OB2_CC_DEBOUNCE_MS)); + CHECK(hold.stable == static_cast(Ob2CcLevel::A1500)); +} + +TEST_CASE("button edge requires the debounce interval") { + Ob2Hold hold; + ob2_hold_init(&hold); + CHECK_FALSE(ob2_hold_feed(&hold, 1, 0, OB2_BUTTON_DEBOUNCE_MS)); + CHECK_FALSE(ob2_hold_feed(&hold, 1, 24, OB2_BUTTON_DEBOUNCE_MS)); + CHECK(ob2_hold_feed(&hold, 1, 25, OB2_BUTTON_DEBOUNCE_MS)); + CHECK(hold.stable == 1); + CHECK_FALSE(ob2_hold_feed(&hold, 1, 80, OB2_BUTTON_DEBOUNCE_MS)); +} + +TEST_CASE("isense is millivolts over 4 and unspecified below 100 mA") { + CHECK(ob2_isense_ma(0) == 0); + CHECK_FALSE(ob2_isense_specified(ob2_isense_ma(396))); + CHECK(ob2_isense_ma(396) == 99); + CHECK(ob2_isense_ma(400) == 100); + CHECK(ob2_isense_specified(100)); + CHECK(ob2_isense_ma(1400) == 350); + CHECK(ob2_isense_ma(2500) == 625); +} + +TEST_CASE("MCP4725 code matches mV times 4096 over 5000") { + CHECK(OB2_MCP4725_ADDR == 0x62); + CHECK(ob2_mcp4725_code(0) == 0); + CHECK(ob2_mcp4725_code(500) == 409); + CHECK(ob2_mcp4725_code(2000) == 1638); + CHECK(ob2_mcp4725_code(5000) == 4095); + CHECK(ob2_mcp4725_code(6000) == 4095); + CHECK(OB2_BIAS_BRINGUP_MV == 500); +} + +TEST_CASE("EEPROM bring-up address is the SOT-23 part with no address pins") { + CHECK(OB2_EEPROM_ADDR == 0x50); + CHECK(OB2_SI5351_ADDR == 0x60); + CHECK(OB2_EEPROM_MAGIC_LEN == 4); + CHECK(OB2_EEPROM_MAGIC[0] == 'O'); + CHECK(OB2_EEPROM_MAGIC[1] == 'B'); + CHECK(OB2_EEPROM_MAGIC[2] == '2'); + CHECK(OB2_EEPROM_MAGIC[3] == '1'); + CHECK(OB2_TWI_HZ == 100000UL); +} + +TEST_CASE("datasheet timer check keeps relay PWM out of the firmware") { + CHECK_FALSE(OB2_RELAY_PWM_SHIPPED); + CHECK_FALSE(OB2_TCA0_SERVES_PA5_AND_PB1); + CHECK_FALSE(OB2_TCB_DRIVES_RELAY_PINS); + CHECK(OB2_TIMER_PIN_COUNT == 8); + + bool pa5_tca = false; + bool pa5_tcd = false; + bool pa6_tcd = false; + bool pa6_other = false; + bool pb1_tca1 = false; + bool pb1_tca0 = false; + bool pa4_woa = false; + bool pb0_tca1 = false; + for (uint8_t i = 0; i < OB2_TIMER_PIN_COUNT; ++i) { + const Ob2TimerPin& row = OB2_TIMER_PINS[i]; + CHECK(std::strncmp(row.timer, "TCB", 3) != 0); + if (row.port == 'A' && row.bit == 5 && std::strcmp(row.timer, "TCA0") == 0) { + pa5_tca = true; + CHECK(std::strcmp(row.signal, "WO5") == 0); + CHECK(std::strcmp(row.portmux, "PORTA") == 0); + } + if (row.port == 'A' && row.bit == 5 && std::strcmp(row.timer, "TCD0") == 0) { + pa5_tcd = true; + CHECK(std::strcmp(row.signal, "WOB") == 0); + } + if (row.port == 'A' && row.bit == 6) { + CHECK(std::strcmp(row.timer, "TCD0") == 0); + CHECK(std::strcmp(row.signal, "WOC") == 0); + pa6_tcd = true; + pa6_other = std::strcmp(row.timer, "TCD0") != 0; + } + if (row.port == 'B' && row.bit == 1 && std::strcmp(row.timer, "TCA1") == 0) { + pb1_tca1 = true; + CHECK(std::strcmp(row.signal, "WO1") == 0); + } + if (row.port == 'B' && row.bit == 1 && std::strcmp(row.timer, "TCA0") == 0) { + pb1_tca0 = true; + CHECK(std::strcmp(row.portmux, "PORTB") == 0); + } + if (row.port == 'A' && row.bit == 4) { + pa4_woa = true; + CHECK_FALSE(row.bench_candidate); + CHECK(std::strcmp(row.signal, "WOA") == 0); + } + if (row.port == 'B' && row.bit == 0 && std::strcmp(row.timer, "TCA1") == 0) { + pb0_tca1 = true; + CHECK_FALSE(row.bench_candidate); + CHECK(std::strcmp(row.signal, "WO0") == 0); + } + } + CHECK(pa5_tca); + CHECK(pa5_tcd); + CHECK(pa6_tcd); + CHECK_FALSE(pa6_other); + CHECK(pb1_tca1); + CHECK(pb1_tca0); + CHECK(pa4_woa); + CHECK(pb0_tca1); +} + +TEST_CASE("stale spec lines stay visible on the pin rows") { + const char* band_b = nullptr; + const char* band_c = nullptr; + for (const Ob2Pin& pin : OB2_PINS) { + if (pin.port == 'A' && pin.bit == 6) { + band_b = pin.role; + } + if (pin.port == 'B' && pin.bit == 1) { + band_c = pin.role; + } + } + REQUIRE(band_b != nullptr); + REQUIRE(band_c != nullptr); + CHECK(std::strstr(band_b, "K2/K5") != nullptr); + CHECK(std::strstr(band_b, "K2/K3/K5/K6") != nullptr); + CHECK(std::strstr(band_c, "pull-down") != nullptr); + CHECK(std::strstr(band_c, "RXEN") != nullptr); + CHECK(std::strstr(band_c, "pull-up") == nullptr); +} diff --git a/firmware2/tools/build.sh b/firmware2/tools/build.sh index e27f67cb..95cf14b3 100755 --- a/firmware2/tools/build.sh +++ b/firmware2/tools/build.sh @@ -23,6 +23,13 @@ if ! arduino-cli core list | awk -v ver="$CORE_VER" 'NR > 1 && $1 == "DxCore:meg arduino-cli core install "DxCore:megaavr@${CORE_VER}" fi +if ! arduino-cli lib list | grep -q "Etherkit Si5351.*2\\.4\\.0"; then + arduino-cli lib install "Etherkit Si5351@2.4.0" +fi +if ! arduino-cli lib list | grep -q "U8g2.*2\\.36\\.19"; then + arduino-cli lib install "U8g2@2.36.19" +fi + echo "OB2_FQBN ${FQBN}" echo "OB2_DXCORE ${CORE_VER}" arduino-cli compile --warnings all --fqbn "$FQBN" "$ROOT/OpenBeacon2"