Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/firmware2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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++
Expand Down
187 changes: 169 additions & 18 deletions firmware2/OpenBeacon2/OpenBeacon2.ino
Original file line number Diff line number Diff line change
@@ -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<Ob2Filter>(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<uint8_t>(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<uint8_t>(g_filter + 1));
} else if (button == Ob2BtnDown) {
apply_filter(static_cast<uint8_t>(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<Ob2CcLevel>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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));
}
}
16 changes: 16 additions & 0 deletions firmware2/OpenBeacon2/ob2_bus.h
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>

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;
9 changes: 9 additions & 0 deletions firmware2/OpenBeacon2/ob2_dac.cpp
Original file line number Diff line number Diff line change
@@ -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<uint16_t>((static_cast<uint32_t>(mv) * OB2_MCP4725_FULL_SCALE) /
OB2_MCP4725_VREF_MV);
}
20 changes: 20 additions & 0 deletions firmware2/OpenBeacon2/ob2_dac.h
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>

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);
124 changes: 124 additions & 0 deletions firmware2/OpenBeacon2/ob2_display.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#include "ob2_hal.h"

#include <SPI.h>
#include <U8g2lib.h>

/*
* 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());
}
Loading
Loading