diff --git a/.github/workflows/flash-budget.yml b/.github/workflows/flash-budget.yml new file mode 100644 index 0000000..148e1a3 --- /dev/null +++ b/.github/workflows/flash-budget.yml @@ -0,0 +1,46 @@ +# Uno flash ceilings for the two minimal sketches. A missing tool, a failed +# compile, an unparsed size line, or a sketch over its budget fails the job. +# The numbers are the measured text+data sizes of the fixtures in +# tests/regression/flash/sketches. Each fixture is its own baseline. +# min_runtime is a new sketch; the audit runtime sketch was not saved. +name: Uno flash budget + +on: + pull_request: + push: + branches-ignore: [master] + workflow_dispatch: + +permissions: + contents: read + +jobs: + flash-budget: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - name: Install pinned arduino-cli 1.5.1 + run: | + set -euo pipefail + curl -fsSL -o /tmp/arduino-cli.tar.gz \ + https://downloads.arduino.cc/arduino-cli/arduino-cli_1.5.1_Linux_64bit.tar.gz + mkdir -p "$HOME/bin" + tar -xzf /tmp/arduino-cli.tar.gz -C "$HOME/bin" arduino-cli + "$HOME/bin/arduino-cli" version | grep -q 'Version: 1.5.1' + echo "$HOME/bin" >> "$GITHUB_PATH" + - name: Install pinned arduino:avr 1.8.6 (avr-gcc 7.3.0) + run: | + set -euo pipefail + arduino-cli core update-index + arduino-cli core install arduino:avr@1.8.6 + test -x "$HOME/.arduino15/packages/arduino/tools/avr-gcc/7.3.0-atmel3.6.1-arduino7/bin/avr-gcc" + - name: Check flash budgets and the over-budget probe + run: tests/regression/flash/check_flash_budget.sh + env: + FLASH_OUT: ${{ runner.temp }}/flash-budget + - uses: actions/upload-artifact@v4 + if: always() + with: + name: flash-budget + path: ${{ runner.temp }}/flash-budget diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index 08e18fb..cee022b 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -1,4 +1,6 @@ -# Report-only regression tests (not a required check; never gates merges). +# Regression harness. The harness step has no continue-on-error: a nonzero +# exit from run_v231_checks.sh fails the job. "Report-only" in the workflow +# name means branch protection does not require this check to merge. # Harness: tests/regression, from Test Lab's Si5351 regression suite. name: Regression tests (report-only) @@ -23,6 +25,11 @@ jobs: - name: Run regression harness (native g++ against src/si5351.cpp) working-directory: tests/regression run: ./run_v231_checks.sh --src "$GITHUB_WORKSPACE" --out "$RUNNER_TEMP/regression" + - name: Prove the pinned v2.4.2 gate rejects a one-line library mutation + working-directory: tests/regression + run: ./reference/prove_v242_negative.sh + env: + NEG_OUT: ${{ runner.temp }}/v242-negative - name: Summary if: always() run: | diff --git a/tests/regression/README.md b/tests/regression/README.md index 0b41477..5045400 100644 --- a/tests/regression/README.md +++ b/tests/regression/README.md @@ -33,8 +33,10 @@ Exit code: 0 = every check PASS/XFAIL/SKIP/INFO; 1 = at least one unexpected FAI | `t106_twowire` | issue #106 TwoWire bus-selection test (`issue106/t106.cpp`, multi-bus stub). SKIP when the source has no `TwoWire*` constructor | yes | | `rdiv_xtal` | CLK0-5 R-divider octaves and CLK6/7 `set_freq()` register 92 edges (`rdiv/t_rdiv.cpp`, issue #89), Register 183 XTAL_CL, and README numbers vs the header / `SI5351_FOUT_MAX` | yes | | `reset_freq` | `reset_freq()` host test (`reset_freq/t_reset_freq.cpp`): re-enable on the next `set_freq()`, invalid index is a no-op, other clocks untouched. SKIP when the source has no `reset_freq()` | yes | +| `ctrl_status` | Exact register bytes and ordered I2C for every legal `drive_strength`, `set_clock_pwr` / `invert` / `source` / `disable` / `fanout` value, status-bit decode, NACK, and `requestFrom` with no payload (`ctrl/t_ctrl.cpp`) | yes | +| `plan_branches` | CLK6/7 `set_freq_manual` rejects, `multisynth67_calc` a±2 and the uncalled pll≠0 arm, `set_ms` CLK6/7 and DIVBY4 exit, `set_vcxo` ppm/`ref_ok`, `set_pll` clamp (`plan/t_plan.cpp`). Locks current v2.4.2 behavior, including the uint8 p1 truncation | yes | | `goldens27_vs_lib` | C++ equivalence harness (`equiv/harness.cpp`) on the pinned source vs the 27 original golden cases | yes (INFO under `--require-fixed`) | -| `equiv_vs_master_1ddd43e` | Full harness transcript diff against vendored master 1ddd43e (`reference/`). 0 means behaviourally identical | INFO | +| `v242_provenance`, `equiv_vs_v242` | `sha256sum -c` of `reference/v242_f885fca/` (git show of f885fca, not a fetch of master), then a full harness transcript diff. 0 differing runs is PASS; any difference is FAIL. `reference/master_1ddd43e/` is no longer the behavior gate | yes | | `goldens_newer_vs_lib` | Newer `lib_*` golden register maps | INFO | | `py_test_golden`, `py_test_spec` | Python golden/spec suites: self-consistency of the AN619 oracle and goldens used above (library-independent) | yes | | `cxx98_syntax` | `-std=c++98 -pedantic -fsyntax-only` warning count | INFO | @@ -50,6 +52,7 @@ Exit code: 0 = every check PASS/XFAIL/SKIP/INFO; 1 = at least one unexpected FAI ## Layout `v231/` acceptance runner, scenarios, oracle, locks, and reports · `stubs/` Arduino/Wire host stubs · `issue106/` t106 and its multi-bus stubs · `reset_freq/` `reset_freq()` regression · -`equiv/` harness, goldens snapshot, compare.py · `reference/master_1ddd43e/` reference source · `python/` golden/spec suites and refs · +`equiv/` harness, goldens snapshot, compare.py · `reference/v242_f885fca/` pinned v2.4.2 source (the hard gate) · `reference/master_1ddd43e/` historical source, not the gate · +`ctrl/` `plan/` branch locks · `flash/check_flash_budget.sh` Uno byte ceilings (separate CI workflow) · `python/` golden/spec suites and refs · `sketches/` Ctor106 · `boards.txt` board set and Boards Manager URLs · `tools/summarize.py`. `v231/Makefile` is the original manual build, kept for reference only; the script does not use it. diff --git a/tests/regression/ctrl/t_ctrl.cpp b/tests/regression/ctrl/t_ctrl.cpp new file mode 100644 index 0000000..3e767d2 --- /dev/null +++ b/tests/regression/ctrl/t_ctrl.cpp @@ -0,0 +1,275 @@ +/* Gating checks for the control-register and status API on v2.4.2. + * + * Every value exercised is a named enumerator or the documented 0/1 enable + * argument. Out-of-range enum casts are undefined (CWG 1766) and are not + * used. The switch defaults in drive_strength and set_pll_input, and the + * clk>CLK7 return in set_clock_disable, stay uncovered for that reason. + * + * Register files are seeded with a non-default byte so a read-modify-write + * that drops unrelated bits fails. + */ +#include "si5351.h" +#include "sim.h" +#include +#include +#include +#include + +static int fails = 0; +#define CHECK(c, msg) do { \ + int _ok = (c); \ + printf("%s %s\n", _ok ? "PASS" : "FAIL", msg); \ + if (!_ok) fails++; \ +} while (0) + +static Si5351 *boot(int pattern) +{ + static Si5351 obj; + g_sim.reset(pattern, true); + obj.~Si5351(); + Si5351 *s = new (&obj) Si5351(); + CHECK(s->init(SI5351_CRYSTAL_LOAD_8PF, 0, 0), "init"); + g_sim.events.clear(); + s->get_i2c_error(); + return s; +} + +static int last_write(uint8_t *reg, uint8_t *val) +{ + for (int i = (int)g_sim.events.size() - 1; i >= 0; i--) { + unsigned r, n; + unsigned b; + if (sscanf(g_sim.events[i].c_str(), "W a=60 r=%u n=%u [%x]", &r, &n, &b) == 3 + && n == 1) { + *reg = (uint8_t)r; + *val = (uint8_t)b; + return 1; + } + } + return 0; +} + +static int saw_read(uint8_t reg) +{ + char needle[32]; + snprintf(needle, sizeof needle, "PTR a=60 r=%u", reg); + for (size_t i = 0; i < g_sim.events.size(); i++) + if (g_sim.events[i] == needle) return 1; + return 0; +} + +/* Read-modify-write traffic: pointer, then the data read, then the data write. + * The read line carries the device address, not the register (that is the PTR). */ +static int ordered_rmw(uint8_t reg, uint8_t val) +{ + int ptr = -1, rd = -1, wr = -1; + char pbuf[32]; + snprintf(pbuf, sizeof pbuf, "PTR a=60 r=%u", reg); + for (int i = 0; i < (int)g_sim.events.size(); i++) { + const std::string &e = g_sim.events[i]; + if (e == pbuf) ptr = i; + if (e.compare(0, 9, "R a=60 n=") == 0) rd = i; + unsigned r, n, b; + if (sscanf(e.c_str(), "W a=60 r=%u n=%u [%x]", &r, &n, &b) == 3 + && r == reg && n == 1 && b == val) wr = i; + } + return ptr >= 0 && rd > ptr && wr > rd; +} + +int main(void) +{ + const si5351_drive drives[4] = { + SI5351_DRIVE_2MA, SI5351_DRIVE_4MA, SI5351_DRIVE_6MA, SI5351_DRIVE_8MA + }; + const uint8_t drive_bits[4] = {0x00, 0x01, 0x02, 0x03}; + + /* Drive strength, every output, every legal level. Seed 0xA5. */ + for (int clk = 0; clk < 8; clk++) { + for (int d = 0; d < 4; d++) { + Si5351 *si = boot(0); + uint8_t addr = (uint8_t)(SI5351_CLK0_CTRL + clk); + g_sim.regs[addr] = 0xA5; + g_sim.events.clear(); + si->drive_strength((si5351_clock)clk, drives[d]); + uint8_t expect = (uint8_t)((0xA5 & ~0x03) | drive_bits[d]); + uint8_t wr, wv; + char buf[96]; + snprintf(buf, sizeof buf, "drive CLK%d %dmA reg", clk, 2 * (d + 1)); + CHECK(last_write(&wr, &wv) && wr == addr && wv == expect + && g_sim.regs[addr] == expect && ordered_rmw(addr, expect), buf); + CHECK((g_sim.regs[addr] & ~0x03) == (0xA5 & ~0x03), "drive preserves other bits"); + } + } + + /* Power and invert, both states, CLK0 and CLK7, from 0xA5. */ + for (int clk = 0; clk <= 7; clk += 7) { + uint8_t addr = (uint8_t)(SI5351_CLK0_CTRL + clk); + Si5351 *si = boot(0); + g_sim.regs[addr] = 0xA5; + g_sim.events.clear(); + si->set_clock_pwr((si5351_clock)clk, 1); + CHECK(g_sim.regs[addr] == (uint8_t)(0xA5 & 0x7F), "pwr on clears only PDN"); + g_sim.events.clear(); + si->set_clock_pwr((si5351_clock)clk, 0); + CHECK(g_sim.regs[addr] == 0xA5, "pwr off sets only PDN"); + g_sim.regs[addr] = 0xA5; + g_sim.events.clear(); + si->set_clock_invert((si5351_clock)clk, 1); + CHECK(g_sim.regs[addr] == (uint8_t)(0xA5 | SI5351_CLK_INVERT), "invert on"); + CHECK(saw_read(addr), "invert read before write"); + si->set_clock_invert((si5351_clock)clk, 0); + CHECK(g_sim.regs[addr] == 0xA5, "invert off restores seed"); + } + + /* Clock source. CLK0 + MS0 returns without a write (defined, not a cast). */ + { + const si5351_clock_source srcs[4] = { + SI5351_CLK_SRC_XTAL, SI5351_CLK_SRC_CLKIN, + SI5351_CLK_SRC_MS0, SI5351_CLK_SRC_MS + }; + const uint8_t bits[4] = { + SI5351_CLK_INPUT_XTAL, SI5351_CLK_INPUT_CLKIN, + SI5351_CLK_INPUT_MULTISYNTH_0_4, SI5351_CLK_INPUT_MULTISYNTH_N + }; + for (int s = 0; s < 4; s++) { + Si5351 *si = boot(0); + g_sim.regs[SI5351_CLK0_CTRL] = 0xA5; + unsigned long w0 = g_sim.n_writes; + g_sim.events.clear(); + si->set_clock_source(SI5351_CLK0, srcs[s]); + if (srcs[s] == SI5351_CLK_SRC_MS0) { + /* The read still happens; the early return must not write. */ + CHECK(g_sim.n_writes == w0 && g_sim.regs[SI5351_CLK0_CTRL] == 0xA5 + && saw_read(SI5351_CLK0_CTRL), "CLK0 MS0 source reads and does not write"); + } else { + uint8_t expect = (uint8_t)((0xA5 & ~SI5351_CLK_INPUT_MASK) | bits[s]); + CHECK(g_sim.regs[SI5351_CLK0_CTRL] == expect, "CLK0 source field"); + CHECK((g_sim.regs[SI5351_CLK0_CTRL] & ~SI5351_CLK_INPUT_MASK) + == (0xA5 & ~SI5351_CLK_INPUT_MASK), "CLK0 source preserves other bits"); + } + } + /* CLK4 can take MS0 (the MS4 mirror encoding) and its own MS. */ + for (int s = 0; s < 4; s++) { + Si5351 *si = boot(0); + uint8_t addr = SI5351_CLK0_CTRL + 4; + g_sim.regs[addr] = 0xA5; + si->set_clock_source(SI5351_CLK4, srcs[s]); + uint8_t expect = (uint8_t)((0xA5 & ~SI5351_CLK_INPUT_MASK) | bits[s]); + CHECK(g_sim.regs[addr] == expect, "CLK4 source field"); + } + /* CLK6/CLK7 own-multisynth and XTAL/CLKIN. MS0 is the MS4 code, legal. */ + for (int clk = 6; clk <= 7; clk++) { + for (int s = 0; s < 4; s++) { + Si5351 *si = boot(0); + uint8_t addr = (uint8_t)(SI5351_CLK0_CTRL + clk); + g_sim.regs[addr] = 0x5A; + si->set_clock_source((si5351_clock)clk, srcs[s]); + uint8_t expect = (uint8_t)((0x5A & ~SI5351_CLK_INPUT_MASK) | bits[s]); + CHECK(g_sim.regs[addr] == expect, "CLK6/7 source field"); + } + } + } + + /* Disable state: 4 modes x 8 clocks. Two bits each, other pairs preserved. */ + { + const si5351_clock_disable modes[4] = { + SI5351_CLK_DISABLE_LOW, SI5351_CLK_DISABLE_HIGH, + SI5351_CLK_DISABLE_HI_Z, SI5351_CLK_DISABLE_NEVER + }; + for (int clk = 0; clk < 8; clk++) { + for (int m = 0; m < 4; m++) { + Si5351 *si = boot(0); + uint8_t reg = (clk <= 3) ? SI5351_CLK3_0_DISABLE_STATE + : SI5351_CLK7_4_DISABLE_STATE; + int sh = (clk <= 3) ? clk * 2 : (clk - 4) * 2; + g_sim.regs[reg] = 0xFF; + g_sim.events.clear(); + si->set_clock_disable((si5351_clock)clk, modes[m]); + uint8_t expect = (uint8_t)((0xFF & ~(0x3 << sh)) | (m << sh)); + uint8_t wr, wv; + CHECK(g_sim.regs[reg] == expect, "disable-state field"); + CHECK(last_write(&wr, &wv) && wr == reg && wv == expect && ordered_rmw(reg, expect), + "disable-state ordered read then write"); + } + } + } + + /* Fanout: 3 sources x enable/disable. Ten coverage branches include the + * switch's untaken default, which would require an out-of-range enum. */ + { + const si5351_clock_fanout fans[3] = { + SI5351_FANOUT_CLKIN, SI5351_FANOUT_XO, SI5351_FANOUT_MS + }; + const uint8_t fbit[3] = { + SI5351_CLKIN_ENABLE, SI5351_XTAL_ENABLE, SI5351_MULTISYNTH_ENABLE + }; + for (int f = 0; f < 3; f++) { + Si5351 *si = boot(0); + g_sim.regs[SI5351_FANOUT_ENABLE] = 0x2A; + g_sim.events.clear(); + si->set_clock_fanout(fans[f], 1); + CHECK(g_sim.regs[SI5351_FANOUT_ENABLE] == (uint8_t)(0x2A | fbit[f]) + && ordered_rmw(SI5351_FANOUT_ENABLE, (uint8_t)(0x2A | fbit[f])), "fanout on"); + si->set_clock_fanout(fans[f], 0); + CHECK(g_sim.regs[SI5351_FANOUT_ENABLE] == (uint8_t)(0x2A & ~fbit[f]), "fanout off"); + CHECK((g_sim.regs[SI5351_FANOUT_ENABLE] & ~fbit[f]) == (0x2A & ~fbit[f]), + "fanout preserves unrelated bits"); + } + } + + /* Status decode. Register 0 = 0xAE, register 1 = 0x51. */ + { + Si5351 *si = boot(0); + g_sim.regs[SI5351_DEVICE_STATUS] = 0xAE; + g_sim.regs[SI5351_INTERRUPT_STATUS] = 0x51; + g_sim.events.clear(); + si->update_status(); + CHECK(si->dev_status.SYS_INIT == 1 && si->dev_status.LOL_B == 0 + && si->dev_status.LOL_A == 1 && si->dev_status.LOS_CLKIN == 0 + && si->dev_status.LOS_XTAL == 1 && si->dev_status.LOS == 1 + && si->dev_status.REVID == 2, "sys status 0xAE"); + CHECK(si->dev_int_status.SYS_INIT_STKY == 0 && si->dev_int_status.LOL_B_STKY == 1 + && si->dev_int_status.LOL_A_STKY == 0 && si->dev_int_status.LOS_CLKIN_STKY == 1 + && si->dev_int_status.LOS_XTAL_STKY == 0 && si->dev_int_status.LOS_STKY == 0, + "int status 0x51"); + CHECK(saw_read(SI5351_DEVICE_STATUS) && saw_read(SI5351_INTERRUPT_STATUS), + "update_status reads reg 0 then reg 1"); + g_sim.regs[0] = 0x00; + g_sim.regs[1] = 0xFF; + si->update_status(); + CHECK(si->dev_status.SYS_INIT == 0 && si->dev_status.REVID == 0 + && si->dev_status.LOS_XTAL == 0 + && si->dev_int_status.SYS_INIT_STKY == 1 + && si->dev_int_status.LOS_XTAL_STKY == 1 + && si->dev_int_status.LOS_STKY == 1, "status zeros and ones"); + } + + /* NACK: device stops acknowledging. endTransmission returns 2. */ + { + Si5351 *si = boot(0); + g_sim.present = false; + g_sim.events.clear(); + uint8_t v = si->si5351_read(SI5351_DEVICE_STATUS); + uint8_t e1 = si->get_i2c_error(); + uint8_t e2 = si->get_i2c_error(); + CHECK(v == 0 && e1 == 2 && e2 == 0, "read NACK sets error 2 and get_i2c_error clears it"); + unsigned long w0 = g_sim.n_writes; + uint8_t we = si->si5351_write(SI5351_CLK0_CTRL, 0x11); + CHECK(we == 2 && si->get_i2c_error() == 2 && g_sim.n_writes == w0 + && g_sim.regs[SI5351_CLK0_CTRL] != 0x11, "write NACK does not store the byte"); + } + + /* requestFrom returns no payload after the register pointer write is ACKed. */ + { + Si5351 *si = boot(0); + g_sim.empty_read = true; + g_sim.regs[SI5351_DEVICE_STATUS] = 0xAE; + uint8_t v = si->si5351_read(SI5351_DEVICE_STATUS); + CHECK(v == 0 && si->get_i2c_error() == SI5351_I2C_ERR_NO_DATA, + "empty requestFrom is SI5351_I2C_ERR_NO_DATA"); + g_sim.empty_read = false; + } + + printf("%s (%d failures)\n", fails ? "FAILED" : "ALL PASS", fails); + return fails ? 1 : 0; +} diff --git a/tests/regression/flash/budgets.txt b/tests/regression/flash/budgets.txt new file mode 100644 index 0000000..98b91a2 --- /dev/null +++ b/tests/regression/flash/budgets.txt @@ -0,0 +1,24 @@ +# Flash ceilings for arduino:avr:uno. A sketch larger than its budget FAILs. +# A smaller sketch still passes: this is a non-regression ceiling, not a claim +# that the library got faster. +# +# Reproduced 2026-11-08 with: +# arduino-cli 1.5.1 (commit 01f3d4f2b) +# arduino:avr 1.8.6 +# avr-gcc 7.3.0-atmel3.6.1-arduino7 +# fqbn arduino:avr:uno +# core flags from platform.txt: -Os -flto -fpermissive -std=gnu++11 +# -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics +# Size method: arduino-cli "Sketch uses N bytes", which is avr-size text+data. +# The checker also requires avr-size text+data == N. A parse miss is a FAIL. +# +# Both numbers below are baselines of the fixtures in sketches/. The audit +# runtime-clock sketch was not saved (REPORT only, no .ino), so its reported +# 12604 bytes cannot be compared with min_runtime and is not a target. +# min_const measures 10830 on this pin. The audit reported 10830 for a sketch +# described the same way; that .ino was not saved either, so 10830 is this +# fixture's measurement. +# min_runtime is a new fixture: global volatile uint8_t clock index, constant +# 14 MHz. Its own baseline on this pin is 11558. +min_const 10830 +min_runtime 11558 diff --git a/tests/regression/flash/check_flash_budget.sh b/tests/regression/flash/check_flash_budget.sh new file mode 100755 index 0000000..92161cc --- /dev/null +++ b/tests/regression/flash/check_flash_budget.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# Fail-closed Uno flash budget for the two minimal v2.4.2 sketches. +# Missing CLI, core, compiler, map, or a "Sketch uses" line is a failure. +# This measures program bytes. It is not a timing benchmark. +set -euo pipefail + +CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ROOT="$(cd "$CI_DIR/../.." && pwd)" +OUT="${FLASH_OUT:-/tmp/si5351-flash-budget}" +CLI="${ARDUINO_CLI:-}" +FQBN="arduino:avr:uno" +CORE_ID="arduino:avr" +CORE_VER="1.8.6" +GCC_REL="7.3.0-atmel3.6.1-arduino7" +CLI_VER="1.5.1" + +if [[ -z "$CLI" ]]; then + if [[ -x /tmp/arduino-cli-bin/arduino-cli ]]; then CLI=/tmp/arduino-cli-bin/arduino-cli + elif [[ -x "$HOME/bin/arduino-cli" ]]; then CLI="$HOME/bin/arduino-cli" + else CLI="$(command -v arduino-cli || true)"; fi +fi + +fail() { echo "FAIL $*" >&2; exit 1; } + +[[ -n "$CLI" && -x "$CLI" ]] || fail "arduino-cli not found (set ARDUINO_CLI). Missing toolchain is a failure, not a skip." +VER="$("$CLI" version 2>&1 || true)" +echo "$VER" | grep -q "Version: ${CLI_VER}" || fail "arduino-cli is not ${CLI_VER}: $VER" + +DATA="${ARDUINO_DIRECTORIES_DATA:-$HOME/.arduino15}" +CORE_LIST="$("$CLI" core list 2>&1 || true)" +echo "$CORE_LIST" | grep -Eq "^${CORE_ID}[[:space:]]+${CORE_VER}([^0-9]|$)" \ + || fail "core ${CORE_ID}@${CORE_VER} is not installed. core list: ${CORE_LIST}" + +GCC="$DATA/packages/arduino/tools/avr-gcc/${GCC_REL}/bin/avr-gcc" +SIZE="$DATA/packages/arduino/tools/avr-gcc/${GCC_REL}/bin/avr-size" +[[ -x "$GCC" && -x "$SIZE" ]] || fail "pinned avr-gcc not at $GCC" +"$GCC" --version | head -1 | grep -q '7\.3\.0' || fail "avr-gcc --version is not 7.3.0: $("$GCC" --version | head -1)" + +rm -rf "$OUT" +mkdir -p "$OUT/logs" "$OUT/elf" +echo "$VER" > "$OUT/logs/arduino-cli-version.txt" +echo "$CORE_LIST" > "$OUT/logs/core-list.txt" +"$GCC" --version > "$OUT/logs/avr-gcc-version.txt" +cp "$DATA/packages/arduino/hardware/avr/${CORE_VER}/platform.txt" "$OUT/logs/platform.txt" + +declare -A BUDGET +while read -r name bytes; do + [[ -z "${name:-}" || "$name" == \#* ]] && continue + [[ "$bytes" =~ ^[0-9]+$ ]] || fail "bad budget line: $name $bytes" + BUDGET[$name]=$bytes +done < "$CI_DIR/flash/budgets.txt" +[[ ${#BUDGET[@]} -ge 1 ]] || fail "no budgets in budgets.txt" + +# gate_log -> prints the byte count, exit 1 if over or unparsed +gate_log() { + local log="$1" budget="$2" n + n="$(grep -oE 'Sketch uses [0-9]+ bytes' "$log" | grep -oE '[0-9]+' | head -1 || true)" + [[ -n "$n" ]] || return 2 + if [[ "$n" -gt "$budget" ]]; then + echo "$n" + return 1 + fi + echo "$n" + return 0 +} + +build_one() { + local name="$1" + local src="$CI_DIR/flash/sketches/${name}.ino.txt" + [[ -f "$src" ]] || fail "missing sketch $src" + [[ -n "${BUDGET[$name]:-}" ]] || fail "no budget for $name (refusing to skip)" + local sk="$OUT/sketch/$name/$name" + mkdir -p "$sk" "$OUT/build/$name" + cp "$src" "$sk/${name}.ino" + local log="$OUT/logs/${name}.log" + if ! "$CLI" compile --fqbn "$FQBN" --library "$ROOT" \ + --build-path "$OUT/build/$name" \ + --build-property "compiler.c.elf.extra_flags=-Wl,-Map,$OUT/build/$name/flash.map" \ + "$sk" > "$log" 2>&1; then + fail "$name did not compile (see $log)" + fi + local elf="$OUT/build/$name/${name}.ino.elf" + local map="$OUT/build/$name/flash.map" + [[ -f "$elf" && -f "$map" ]] || fail "$name produced no ELF or map" + cp "$elf" "$OUT/elf/${name}.elf" + cp "$map" "$OUT/elf/${name}.map" + + local sketch_bytes text data bss + sketch_bytes="$(grep -oE 'Sketch uses [0-9]+ bytes' "$log" | grep -oE '[0-9]+' | head -1 || true)" + [[ -n "$sketch_bytes" ]] || fail "$name: could not parse 'Sketch uses N bytes'" + # avr-size default columns: text data bss + read -r text data bss < <("$SIZE" "$elf" | awk 'NR==2 {print $1, $2, $3}') + [[ -n "${text:-}" && -n "${data:-}" && -n "${bss:-}" ]] || fail "$name: avr-size parse failed" + local sum=$((text + data)) + [[ "$sum" -eq "$sketch_bytes" ]] || fail "$name: Sketch uses $sketch_bytes but avr-size text+data is $sum" + if [[ "$sketch_bytes" -gt "${BUDGET[$name]}" ]]; then + fail "$name uses $sketch_bytes bytes, budget is ${BUDGET[$name]} (not raised)" + fi + echo "PASS $name $sketch_bytes bytes <= budget ${BUDGET[$name]} (text=$text data=$data bss=$bss)" + echo "$name $sketch_bytes ${BUDGET[$name]} $text $data $bss" >> "$OUT/measured.tsv" +} + +for name in min_const min_runtime; do + build_one "$name" +done + +# Negative control: the same parser must reject an over-budget line and a +# real log scored against a 1-byte ceiling. Success of this script requires +# both probes to fail the gate. +probe() { + local label="$1" log="$2" budget="$3" rc=0 + gate_log "$log" "$budget" >/dev/null || rc=$? + [[ "$rc" -eq 1 ]] || fail "negative probe '$label' did not fail over budget (rc=$rc)" + echo "PASS negative probe rejected $label" +} +printf 'Sketch uses 999999 bytes of program storage space.\n' > "$OUT/logs/synthetic-over.log" +probe "synthetic 999999 vs budget 1" "$OUT/logs/synthetic-over.log" 1 +probe "min_const log vs budget 1" "$OUT/logs/min_const.log" 1 +# A missing size line is also a failure (rc 2), not a skip. +printf 'compilation finished\n' > "$OUT/logs/no-size.log" +rc=0 +gate_log "$OUT/logs/no-size.log" 999999 >/dev/null || rc=$? +[[ "$rc" -eq 2 ]] || fail "unparsed size line was not a hard failure (rc=$rc)" +echo "PASS unparsed size line is a failure" +echo "ALL PASS" +exit 0 diff --git a/tests/regression/flash/sketches/min_const.ino.txt b/tests/regression/flash/sketches/min_const.ino.txt new file mode 100644 index 0000000..7585ad4 --- /dev/null +++ b/tests/regression/flash/sketches/min_const.ino.txt @@ -0,0 +1,12 @@ +#include "si5351.h" +#include "Wire.h" + +Si5351 si5351; + +void setup() +{ + si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, 0); + si5351.set_freq(1400000000ULL, SI5351_CLK0); +} + +void loop() {} diff --git a/tests/regression/flash/sketches/min_runtime.ino.txt b/tests/regression/flash/sketches/min_runtime.ino.txt new file mode 100644 index 0000000..43652ea --- /dev/null +++ b/tests/regression/flash/sketches/min_runtime.ino.txt @@ -0,0 +1,19 @@ +#include "si5351.h" +#include "Wire.h" + +Si5351 si5351; + +/* New flash fixture, not a reconstruction of the audit runtime sketch. + * That sketch was not saved (REPORT only). This file is the budget's program: + * a global volatile uint8_t index and a constant 14 MHz. The gate ceiling is + * the size of this file on the pinned Uno toolchain. + */ +volatile uint8_t clk_index = 0; + +void setup() +{ + si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, 0); + si5351.set_freq(1400000000ULL, (si5351_clock)clk_index); +} + +void loop() {} diff --git a/tests/regression/plan/t_plan.cpp b/tests/regression/plan/t_plan.cpp new file mode 100644 index 0000000..6d7b174 --- /dev/null +++ b/tests/regression/plan/t_plan.cpp @@ -0,0 +1,252 @@ +/* Branch locks for v2.4.2 planning code. Behavior is snapshotted as it is. + * + * Unreachable without an out-of-range enum cast (CWG 1766), left uncovered + * on purpose: + * drive_strength default, set_clock_source default, set_pll_input default, + * set_clock_disable's clk>CLK7 return, set_ms's clk>CLK7 else, + * pll_reset / set_ms_source "neither PLL" side, fanout switch default. + * ms67_div's final `return 0` (ratio still >254 after eight even halvings) + * is reached through set_freq_manual, which is public. + * multisynth67_calc's a+=2 path is not reached by set_freq(): that path + * pre-scales the multisynth up to ceil(600 MHz/254) first. The direct call + * is compiled with -Dprivate=public, same as the other host tests, and does + * not change the library. It currently stores a=256 in a uint8_t (p1 becomes + * 0). That is locked and flagged, not "fixed" here. + */ +#include "si5351.h" +#include "sim.h" +#include +#include + +static int fails = 0; +#define CHECK(c, msg) do { \ + int _ok = (c); \ + printf("%s %s\n", _ok ? "PASS" : "FAIL", msg); \ + if (!_ok) fails++; \ +} while (0) + +static Si5351 *boot(void) +{ + static Si5351 obj; + g_sim.reset(0, true); + obj.~Si5351(); + Si5351 *s = new (&obj) Si5351(); + CHECK(s->init(SI5351_CRYSTAL_LOAD_8PF, 0, 0), "init"); + return s; +} + +static int reg_changed(unsigned long writes_before) +{ + return g_sim.n_writes != writes_before; +} + +int main(void) +{ + /* CLK6/7 manual rejects: no MS block write. */ + { + Si5351 *si = boot(); + unsigned long w = g_sim.n_writes; + uint8_t r = si->set_freq_manual(1234567800ULL, 80000000000ULL, SI5351_CLK6); + CHECK(r == 1 && !reg_changed(w), "manual CLK6 non-integer PLL returns 1, no write"); + } + { + Si5351 *si = boot(); + unsigned long w = g_sim.n_writes; + /* 600 MHz / 150 MHz = 4, even but below 6. */ + uint8_t r = si->set_freq_manual(15000000000ULL, 60000000000ULL, SI5351_CLK7); + CHECK(r == 1 && !reg_changed(w), "manual CLK7 ratio 4 returns 1, no write"); + } + { + Si5351 *si = boot(); + unsigned long w = g_sim.n_writes; + /* 510 MHz / 2 MHz = 255, odd and above 254. */ + uint8_t r = si->set_freq_manual(200000000ULL, 51000000000ULL, SI5351_CLK6); + CHECK(r == 1 && !reg_changed(w), "manual CLK6 ratio 255 returns 1, no write"); + } + { + Si5351 *si = boot(); + unsigned long w = g_sim.n_writes; + /* 65536 Hz * 65536 = 2^32 Hz worth of ratio steps: eight even + * halvings still leave n>254, ms67_div returns 0. */ + uint8_t r = si->set_freq_manual(6553600ULL, 65536ULL * 6553600ULL, SI5351_CLK6); + CHECK(r == 1 && !reg_changed(w), "manual CLK6 ratio 65536 returns 1, no write"); + } + + /* Public set_freq hits multisynth67_calc's a-=2 adjustment. + * 4.6 MHz -> even a=196, product 901.6 MHz, then a-=2 -> 194. */ + { + Si5351 *si = boot(); + uint8_t r = si->set_freq(460000000ULL, SI5351_CLK6); + CHECK(r == 0, "set_freq 4.6 MHz CLK6"); + CHECK(g_sim.regs[SI5351_CLK6_PARAMETERS] == 194, "CLK6 MS ratio 194 after a-=2"); + CHECK(si->pllb_freq == 194ULL * 460000000ULL, "PLLB is 194 * 4.6 MHz"); + } + + /* Direct a+=2. Locked current behavior: a becomes 256 and the uint8_t + * parameter register is 0. A refactor that stops the truncation must + * change this assertion on purpose. */ + { + Si5351RegSet reg = {0, 0, 0}; + uint64_t freq = 500000ULL * SI5351_FREQ_MULT; + Si5351 *si = boot(); + uint64_t got = si->multisynth67_calc(freq, 0, ®); + CHECK(reg.p1 == 0 && reg.p2 == 0 && reg.p3 == 0, "LOCKED: a+=2 to 256 truncates p1 to 0"); + CHECK(got == 256ULL * freq, "LOCKED: returned VCO uses untruncated a=256"); + printf("INFO multisynth67_calc a+=2 stores p1 in uint8_t; 256 becomes 0. Not changed.\n"); + } + + /* set_vcxo ppm clamp and the ref_ok reject. */ + { + Si5351 *si = boot(); + si->set_vcxo(80000000000ULL, 10); + uint8_t low10 = g_sim.regs[SI5351_VXCO_PARAMETERS_LOW]; + uint8_t mid10 = g_sim.regs[SI5351_VXCO_PARAMETERS_MID]; + uint8_t hi10 = g_sim.regs[SI5351_VXCO_PARAMETERS_HIGH]; + g_sim.reset(0, true); + si = boot(); + si->set_vcxo(80000000000ULL, 30); + CHECK(g_sim.regs[SI5351_VXCO_PARAMETERS_LOW] == low10 + && g_sim.regs[SI5351_VXCO_PARAMETERS_MID] == mid10 + && g_sim.regs[SI5351_VXCO_PARAMETERS_HIGH] == hi10, + "set_vcxo 10 ppm clamps to the 30 ppm bytes"); + si->set_vcxo(80000000000ULL, 255); + CHECK(g_sim.regs[SI5351_VXCO_PARAMETERS_LOW] != low10 + || g_sim.regs[SI5351_VXCO_PARAMETERS_MID] != mid10 + || g_sim.regs[SI5351_VXCO_PARAMETERS_HIGH] != hi10, + "set_vcxo 255 ppm clamps to 240 ppm and differs from 30"); + unsigned long w = g_sim.n_writes; + uint8_t pull = si->vcxo_pull; + si->set_ref_freq(5000000, SI5351_PLL_INPUT_XO); + si->set_vcxo(80000000000ULL, 60); + CHECK(g_sim.n_writes == w && si->vcxo_pull == pull, + "set_vcxo with a 5 MHz reference writes nothing"); + } + + /* pll_calc clamps the divider math to 600..900 MHz, but pll_update stores + * the requested frequency anyway (v2.4.2). Locked, not corrected. */ + { + Si5351 *si = boot(); + si->set_pll(10000000000ULL, SI5351_PLLA); /* 100 MHz request */ + CHECK(si->plla_freq == 10000000000ULL, "LOCKED: set_pll stores the 100 MHz request"); + CHECK(g_sim.regs[29] == 0x0A && g_sim.regs[30] == 0x00, + "LOCKED: PLLA registers are the clamped 600 MHz ratio (a=24)"); + si->set_pll(200000000000ULL, SI5351_PLLB); /* 2 GHz request */ + CHECK(si->pllb_freq == 200000000000ULL, "LOCKED: set_pll stores the 2 GHz request"); + CHECK(g_sim.regs[37] == 0x10 && g_sim.regs[38] == 0x00, + "LOCKED: PLLB registers are the clamped 900 MHz ratio (a=36)"); + printf("INFO set_pll stores an out-of-range request while pll_calc clamps the registers.\n"); + } + + /* Both PLLs occupied above the share limit: second >100 MHz CLK is refused. */ + { + Si5351 *si = boot(); + CHECK(si->set_freq(1000000000ULL, SI5351_CLK6) == 0, "CLK6 holds PLLB"); + CHECK(si->set_freq(12000000000ULL, SI5351_CLK0) == 0, "CLK0 120 MHz holds PLLA"); + uint64_t f1 = si->clk_freq[1]; + unsigned long w = g_sim.n_writes; + uint8_t r = si->set_freq(11000000000ULL, SI5351_CLK1); + CHECK(r == 1 && si->clk_freq[1] == f1 && g_sim.n_writes == w, + "CLK1 110 MHz refused while both PLLs are held"); + } + + /* 160 MHz then 80 MHz leaves DIVBY4 and resets the PLL (reg 177). */ + { + Si5351 *si = boot(); + CHECK(si->set_freq(16000000000ULL, SI5351_CLK0) == 0, "CLK0 160 MHz"); + g_sim.regs[SI5351_PLL_RESET] = 0; + CHECK(si->set_freq(8000000000ULL, SI5351_CLK0) == 0, "CLK0 80 MHz leaves DIVBY4"); + CHECK(g_sim.regs[SI5351_PLL_RESET] == SI5351_PLL_RESET_A, + "leaving DIVBY4 writes PLLA reset"); + } + + /* set_ms on CLK6/CLK7 writes only that clock's one-byte ratio, and ms_div + * updates only that clock's field in register 92. */ + { + Si5351 *si = boot(); + Si5351RegSet ms = {42, 0, 0}; + g_sim.regs[90] = 0x11; + g_sim.regs[91] = 0x22; + g_sim.regs[92] = 0xFF; + si->set_ms(SI5351_CLK6, ms, 0, 3, 0); + CHECK(g_sim.regs[90] == 42 && g_sim.regs[91] == 0x22, "set_ms CLK6 writes reg 90 only"); + CHECK(g_sim.regs[92] == (uint8_t)((0xFF & ~0x07) | 3), "set_ms CLK6 R-div keeps CLK7 bits"); + g_sim.regs[90] = 0x11; + g_sim.regs[92] = 0xFF; + ms.p1 = 40; + si->set_ms(SI5351_CLK7, ms, 0, 5, 0); + CHECK(g_sim.regs[91] == 40 && g_sim.regs[90] == 0x11, "set_ms CLK7 writes reg 91 only"); + CHECK(g_sim.regs[92] == (uint8_t)((0xFF & ~0x70) | (5 << 4)), "set_ms CLK7 R-div keeps CLK6 bits"); + } + + /* set_ms DIVBY4 entry, then the exit that resets PLLA. */ + { + Si5351 *si = boot(); + Si5351RegSet ms = {100, 0, 1}; + g_sim.regs[44] = 0x00; + g_sim.regs[SI5351_PLL_RESET] = 0; + si->set_ms(SI5351_CLK0, ms, 0, 0, 1); + CHECK((g_sim.regs[44] & SI5351_OUTPUT_CLK_DIVBY4) == SI5351_OUTPUT_CLK_DIVBY4, + "set_ms CLK0 div_by_4 sets the DIVBY4 bits"); + CHECK(g_sim.regs[SI5351_PLL_RESET] == 0, "entering DIVBY4 does not reset the PLL"); + si->set_ms(SI5351_CLK0, ms, 0, 0, 0); + CHECK((g_sim.regs[44] & SI5351_OUTPUT_CLK_DIVBY4) == 0, "set_ms clears DIVBY4"); + CHECK(g_sim.regs[SI5351_PLL_RESET] == SI5351_PLL_RESET_A, + "set_ms leaving DIVBY4 resets PLLA"); + } + + /* multisynth67_calc's pll_freq != 0 arm has no caller in v2.4.2 (set_freq + * always passes 0; set_freq_manual uses ms67_div). Locked here so a future + * caller cannot change the reject rules unnoticed. -Dprivate=public is the + * existing host-test switch; the library sources are not edited. */ + { + Si5351 *si = boot(); + Si5351RegSet reg = {9, 9, 9}; + uint64_t got = si->multisynth67_calc(1400000000ULL, 90000000000ULL, ®); + CHECK(got == 0 && reg.p1 == 9, "LOCKED: non-integer PLL/MS returns 0 and does not write p1"); + got = si->multisynth67_calc(15000000000ULL, 60000000000ULL, ®); + CHECK(got == 0, "LOCKED: integer ratio 4 is below 6 and returns 0"); + got = si->multisynth67_calc(100000000ULL, 256ULL * 100000000ULL, ®); + CHECK(got == 0, "LOCKED: integer ratio 256 is above 254 and returns 0"); + got = si->multisynth67_calc(10000000000ULL, 60000000000ULL, ®); + CHECK(got == 1 && reg.p1 == 6 && reg.p2 == 0 && reg.p3 == 0, + "LOCKED: integer ratio 6 returns 1 and stores p1=6"); + reg.p1 = 0; + got = si->multisynth67_calc(20000000000ULL, 0, ®); + CHECK(reg.p1 == 6 && got == 90000000000ULL, + "LOCKED: freq above 150 MHz is clamped before the integer search"); + got = si->multisynth67_calc(100000ULL, 0, ®); + CHECK(reg.p1 == 0 && got == 256ULL * 500000ULL * SI5351_FREQ_MULT, + "LOCKED: freq below 500 kHz clamps, then a+=2 truncates p1"); + printf("INFO multisynth67_calc(freq, pll!=0) is unreachable from set_freq/set_freq_manual.\n"); + printf("INFO multisynth67_calc a<6 is unreachable: freq is clamped to 150 MHz first, and that ratio is already 6.\n"); + printf("INFO set_freq CLK6/7 reset_owed arm is unreachable: CLK6/7 set_ms never sets reset_owed, and every public path clears it before returning.\n"); + printf("INFO multisynth_calc's lltmp==5 arm is unreachable: that ratio is 180 MHz, which is already DIVBY4.\n"); + printf("INFO pll_calc a<15 is unreachable via set_ref_freq: the setter never stores a crystal above 30 MHz, and 600 MHz/30 MHz is 20.\n"); + printf("INFO switch defaults in drive_strength, set_clock_source, set_pll_input, and set_clock_disable's clk>7 return need an out-of-range enum and are not tested.\n"); + } + + /* pll_calc's a>90 clamp. 7 MHz is inside ref_ok; 900/7 is 128. */ + { + Si5351 *si = boot(); + si->set_ref_freq(7000000, SI5351_PLL_INPUT_XO); + si->set_pll(90000000000ULL, SI5351_PLLA); + CHECK(si->plla_freq == 90000000000ULL, "LOCKED: set_pll still stores the 900 MHz request at 7 MHz"); + CHECK(g_sim.regs[29] == 0x2B && g_sim.regs[30] == 0x00, + "7 MHz reference clamps PLL feedback a from 128 to 90"); + } + + /* multisynth_calc's >200 MHz clamp. set_freq clamps before it calls this, + * so the line is only reached by a direct call. Same -Dprivate=public + * switch as the rest of the host tests. */ + { + Si5351 *si = boot(); + Si5351RegSet hi, cap; + si->multisynth_calc(21000000000ULL, 0, &hi); + si->multisynth_calc(20000000000ULL, 0, &cap); + CHECK(hi.p1 == cap.p1 && hi.p2 == cap.p2 && hi.p3 == cap.p3, + "LOCKED: multisynth_calc above 200 MHz matches the 200 MHz parameters"); + } + + printf("%s (%d failures)\n", fails ? "FAILED" : "ALL PASS", fails); + return fails ? 1 : 0; +} diff --git a/tests/regression/reference/prove_v242_negative.sh b/tests/regression/reference/prove_v242_negative.sh new file mode 100755 index 0000000..cdf6070 --- /dev/null +++ b/tests/regression/reference/prove_v242_negative.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Scratch mutation of the pinned v2.4.2 library. The hard gate must report +# a non-zero differing-run count. This script exits 0 only when that happens. +# It does not modify the worktree and it does not fetch a branch. +set -euo pipefail + +CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REF="$CI_DIR/reference/v242_f885fca" +CXX="${CXX:-g++}" +OUT="${NEG_OUT:-/tmp/si5351-v242-negative}" +command -v "$CXX" >/dev/null || { echo "missing $CXX" >&2; exit 2; } + +( cd "$REF" && sha256sum -c SHA256SUMS ) || { echo "FAIL pinned reference hash" >&2; exit 1; } + +rm -rf "$OUT" +mkdir -p "$OUT" +cp "$REF/si5351.cpp" "$OUT/si5351.cpp" +cp "$REF/si5351.h" "$OUT/si5351.h" +python3 - "$OUT/si5351.cpp" <<'PY' +import sys +p = sys.argv[1] +s = open(p).read() +old = "reg_val &= ~(1<<(uint8_t)clk);" +n = s.count(old) +if n != 1: + sys.stderr.write("mutation site count is %d, expected 1\n" % n) + sys.exit(2) +# Flip output_enable()'s enable path so a requested enable sets the OEB bit. +s = s.replace(old, "reg_val |= (1<<(uint8_t)clk);", 1) +open(p, "w").write(s) +PY + +CXXFLAGS="-std=gnu++17 -O1 -g -Wall -Wno-unused-variable -Wno-unused-result" +echo 'int main(){return 0;}' > "$OUT/probe.cpp" +if $CXX -ftrivial-auto-var-init=pattern "$OUT/probe.cpp" -o "$OUT/probe" 2>/dev/null; then + CXXFLAGS="$CXXFLAGS -ftrivial-auto-var-init=pattern" +fi + +$CXX $CXXFLAGS -I"$CI_DIR/equiv" -I"$CI_DIR/stubs" -I"$OUT" \ + "$CI_DIR/equiv/harness.cpp" "$CI_DIR/stubs/Wire.cpp" "$OUT/si5351.cpp" \ + -o "$OUT/h_mut" > "$OUT/build_mut.log" 2>&1 +$CXX $CXXFLAGS -I"$CI_DIR/equiv" -I"$CI_DIR/stubs" -I"$REF" \ + "$CI_DIR/equiv/harness.cpp" "$CI_DIR/stubs/Wire.cpp" "$REF/si5351.cpp" \ + -o "$OUT/h_pin" > "$OUT/build_pin.log" 2>&1 +"$OUT/h_mut" "$OUT/mut.jsonl" > "$OUT/run_mut.log" 2>&1 +"$OUT/h_pin" "$OUT/pin.jsonl" > "$OUT/run_pin.log" 2>&1 +python3 "$CI_DIR/equiv/compare.py" "$OUT/mut.jsonl" "$OUT/pin.jsonl" "$OUT/equiv_report.txt" \ + > "$OUT/compare.log" 2>&1 || true +line="$(grep -E '^Equivalence:' "$OUT/equiv_report.txt" || true)" +echo "$line" +diffs="$(printf '%s\n' "$line" | sed -n 's/.*differing runs: \([0-9][0-9]*\).*/\1/p')" +if [[ -z "$diffs" ]]; then + echo "FAIL negative control produced no equivalence summary" >&2 + exit 1 +fi +if [[ "$diffs" -eq 0 ]]; then + echo "FAIL mutation was not detected (differing runs: 0)" >&2 + exit 1 +fi +echo "PASS negative control: mutated output_enable differs in $diffs runs" +exit 0 diff --git a/tests/regression/reference/v242_f885fca/PROVENANCE b/tests/regression/reference/v242_f885fca/PROVENANCE new file mode 100644 index 0000000..f2225cb --- /dev/null +++ b/tests/regression/reference/v242_f885fca/PROVENANCE @@ -0,0 +1,16 @@ +Pinned Si5351Arduino v2.4.2 library sources for the hard equivalence gate. +Do not edit these files. tests/regression/run_v231_checks.sh runs +`sha256sum -c SHA256SUMS` in this directory before it compiles them, and a +mismatch is a FAIL. This copy is not part of the coverage measurement. + +commit: f885fca853b9365d4f2e633267971b2b758b05ae +subject: Merge pull request #115 from etherkit/release-2.4.2 +library.properties version: 2.4.2 +extracted: git show f885fca853b9365d4f2e633267971b2b758b05ae:src/si5351.cpp + git show f885fca853b9365d4f2e633267971b2b758b05ae:src/si5351.h +sha256 si5351.cpp: f14fa6275391403ce085a10cc007f679d61faab38dec69d660942ae704e9fcdb +sha256 si5351.h: f934e91d084d1c76ab45ec7a502c6e7cea2838102f23b33dcbb233f85ccddaa6 + +The gate compares the harness transcript of the library under test with the +transcript of this exact source. Zero differing runs is PASS. It does not +fetch master, and it does not use reference/master_1ddd43e. diff --git a/tests/regression/reference/v242_f885fca/SHA256SUMS b/tests/regression/reference/v242_f885fca/SHA256SUMS new file mode 100644 index 0000000..9cb0089 --- /dev/null +++ b/tests/regression/reference/v242_f885fca/SHA256SUMS @@ -0,0 +1,2 @@ +f14fa6275391403ce085a10cc007f679d61faab38dec69d660942ae704e9fcdb si5351.cpp +f934e91d084d1c76ab45ec7a502c6e7cea2838102f23b33dcbb233f85ccddaa6 si5351.h diff --git a/tests/regression/reference/v242_f885fca/si5351.cpp b/tests/regression/reference/v242_f885fca/si5351.cpp new file mode 100644 index 0000000..f1fbb42 --- /dev/null +++ b/tests/regression/reference/v242_f885fca/si5351.cpp @@ -0,0 +1,1930 @@ +/* + * si5351.cpp - Si5351 library for Arduino + * + * Copyright (C) 2015 - 2019 Jason Milldrum + * Dana H. Myers + * + * Some tuning algorithms derived from clk-si5351.c in the Linux kernel. + * Sebastian Hesselbarth + * Rabeeh Khoury + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include "Arduino.h" +#include "Wire.h" +#include "si5351.h" + + +/*********************/ +/* File-local helpers */ +/*********************/ + +/* Pack p1/p2/p3 into the 8-byte Si5351 parameter block used by PLL/MS regs. + * p1_extra is ORed into the p1[17:16] byte (byte 2); set_ms uses this to + * preserve other bits already in that register. set_pll/set_vcxo pass 0. */ +// Set while set_freq() re-plans a PLL: it resets that PLL once after all +// PLL and MS writes, so set_ms() must not issue its own DIVBY4-exit reset. +static uint8_t defer_pll_reset; +// DIVBY4 exit seen while defer_pll_reset was set (reset still owed) +static uint8_t reset_owed; +// Clock whose MS the caller is about to write itself (skipped by recalc) +static uint8_t recalc_skip = 0xFF; +// Make pll_update() recalculate even if the PLL frequency is unchanged +static uint8_t recalc_force; + +// Datasheet / AN619 maximum output (and MS0-5 output) frequency. The public +// SI5351_MULTISYNTH_MAX_FREQ / SI5351_CLKOUT_MAX_FREQ (225 MHz) are kept for +// compatibility but are above the spec; DIVBY4 covers 150 < Fout <= 200 MHz. +#define SI5351_FOUT_MAX 200000000ULL + +// MS6/MS7 divide ratios are even integers 6..254 (AN619 sec. 2.2, 4.1.2), so +// with fVCO >= 600 MHz the MS6/7 output is at least 600 MHz / 254 and, with +// R = 128, the lowest CLK6/7 output is 600 MHz / 254 / 128 (rounded up here; +// SI5351_CLKOUT67_MIN_FREQ is rounded down and would give a ratio of 256). +#define SI5351_MS67_MIN_FREQ ((SI5351_PLL_VCO_MIN * SI5351_FREQ_MULT + SI5351_MULTISYNTH67_A_MAX - 1) / SI5351_MULTISYNTH67_A_MAX) +#define SI5351_CLKOUT67_MIN ((SI5351_MS67_MIN_FREQ + 127) / 128) + +// Even-integer MS6/7 ratio (6..254) of pll_freq for an output freq, using the +// smallest R divider that brings the ratio down to 254. Returns the ratio and +// sets *r_div, or returns 0 if no valid ratio/R pair exists. +static uint8_t ms67_div(uint64_t pll_freq, uint64_t freq, uint8_t *r_div) +{ + uint64_t n; + uint8_t r; + + if(freq == 0 || pll_freq % freq) + { + return 0; + } + n = pll_freq / freq; + + for(r = 0; r <= 7; r++) + { + if(n <= SI5351_MULTISYNTH67_A_MAX) + { + if(n < SI5351_MULTISYNTH_A_MIN || (n & 1)) + { + return 0; + } + *r_div = r; + return (uint8_t)n; + } + if(n & 1) + { + return 0; + } + n >>= 1; + } + + return 0; +} + +// AN619: feedback ratio 15..90 and VCO 600..900 MHz. A reference outside +// 6.67..60 MHz (e.g. 5 MHz) has no valid setting; such PLLs are not written. +static uint8_t ref_ok(uint32_t ref) +{ + return (uint64_t)ref * SI5351_PLL_A_MAX >= SI5351_PLL_VCO_MIN + && (uint64_t)ref * SI5351_PLL_A_MIN <= SI5351_PLL_VCO_MAX; +} + +static void pack_reg_set(const struct Si5351RegSet *reg, uint8_t *params, uint8_t p1_extra) +{ + params[0] = (uint8_t)((reg->p3 >> 8) & 0xFF); + params[1] = (uint8_t)(reg->p3 & 0xFF); + params[2] = (uint8_t)(((reg->p1 >> 16) & 0x03) | p1_extra); + params[3] = (uint8_t)((reg->p1 >> 8) & 0xFF); + params[4] = (uint8_t)(reg->p1 & 0xFF); + params[5] = (uint8_t)(((reg->p3 >> 12) & 0xF0) | ((reg->p2 >> 16) & 0x0F)); + params[6] = (uint8_t)((reg->p2 >> 8) & 0xFF); + params[7] = (uint8_t)(reg->p2 & 0xFF); +} + +/* Fractional feedback a + b/c -> {p1,p2,p3}; compute (128*b)/c once. */ +static void frac_to_reg(uint32_t a, uint32_t b, uint32_t c, struct Si5351RegSet *reg) +{ + uint32_t rem = (128 * b) / c; + reg->p1 = 128 * a + rem - 512; + reg->p2 = 128 * b - c * rem; + reg->p3 = c; +} + + + +/********************/ +/* Public functions */ +/********************/ + +Si5351::Si5351(TwoWire *wire, uint8_t i2c_addr) +{ + setup_obj(wire, i2c_addr); +} + +// Shared constructor body (no C++11 delegating constructor needed) +void Si5351::setup_obj(TwoWire *wire, uint8_t i2c_addr) +{ + i2c_bus_addr = i2c_addr; + i2c_error = 0; + _wire = wire ? wire : &Wire; + xtal_freq[0] = SI5351_XTAL_FREQ; + + // Start by using XO ref osc as default for each PLL + plla_ref_osc = SI5351_PLL_INPUT_XO; + pllb_ref_osc = SI5351_PLL_INPUT_XO; + clkin_div = SI5351_CLKIN_DIV_1; + vcxo_pull = 0; +} + +/* + * init(uint8_t xtal_load_c, uint32_t ref_osc_freq, int32_t corr) + * + * Setup communications to the Si5351 and set the crystal + * load capacitance. + * + * xtal_load_c - Crystal load capacitance. Use the SI5351_CRYSTAL_LOAD_*PF + * defines in the header file + * xo_freq - Crystal/reference oscillator frequency in 1 Hz increments. + * Defaults to 25000000 if a 0 is used here. + * corr - Frequency correction constant in parts-per-billion + * + * Returns a boolean that indicates whether a device was found on the desired + * I2C address. + * + */ +bool Si5351::init(uint8_t xtal_load_c, uint32_t xo_freq, int32_t corr) +{ + // Start I2C comms on the selected bus. Called with no arguments so that + // pins chosen beforehand (e.g. ESP32 setPins()) are kept; on ESP32 a bus + // already started by the sketch with begin(sda, scl, freq) is left as is. + _wire->begin(); + + // Check for a device on the bus, bail out if it is not there + _wire->beginTransmission(i2c_bus_addr); + uint8_t reg_val; + reg_val = _wire->endTransmission(); + + if(reg_val == 0) + { + // Wait for SYS_INIT flag to be clear, indicating that device is ready + uint8_t status_reg = 0; + do + { + status_reg = si5351_read(SI5351_DEVICE_STATUS); + } while (status_reg >> 7 == 1); + + // Register 183. AN619: XTAL_CL[1:0] 00b is "Reserved. Do not select + // this option." 01b = 6 pF, 10b = 8 pF, 11b = 10 pF (default). + // Bits 5:0 must be written as 010010b. SI5351_CRYSTAL_LOAD_0PF is + // that reserved code; keep the 10 pF default instead of writing it. + xtal_load_c &= SI5351_CRYSTAL_LOAD_MASK; + if(xtal_load_c == SI5351_CRYSTAL_LOAD_0PF) + { + xtal_load_c = SI5351_CRYSTAL_LOAD_10PF; + } + si5351_write(SI5351_CRYSTAL_LOAD, xtal_load_c | 0b00010010); + + // Set up the XO and CLKIN reference frequencies + if (xo_freq != 0) + { + set_ref_freq(xo_freq, SI5351_PLL_INPUT_XO); + set_ref_freq(xo_freq, SI5351_PLL_INPUT_CLKIN); //Also CLKIN + } + else + { + set_ref_freq(SI5351_XTAL_FREQ, SI5351_PLL_INPUT_XO); + set_ref_freq(SI5351_XTAL_FREQ, SI5351_PLL_INPUT_CLKIN); //Also CLKIN + } + + // Set the frequency calibrations for the XO and CLKIN + set_correction(corr, SI5351_PLL_INPUT_XO); + set_correction(corr, SI5351_PLL_INPUT_CLKIN); + + reset(); + + return true; + } + else + { + i2c_error = reg_val; // device not ACKing: make get_i2c_error() report it + return false; + } +} + +/* + * reset(void) + * + * Call to reset the Si5351 to the state initialized by the library. + * + */ +void Si5351::reset(void) +{ + uint8_t i; + + // VCXO is cleared below + vcxo_pull = 0; + + // Initialize the CLK outputs according to flowchart in datasheet + // First, turn them off (and forget them, so set_pll() below does not + // recalculate their dividers) + for(i = 0; i < 8; i++) + { + clk_freq[i] = 0; + si5351_write(SI5351_CLK0_CTRL + i, 0x80); + } + + // Turn the clocks back on... + for(i = 0; i < 8; i++) + { + si5351_write(SI5351_CLK0_CTRL + i, 0x0c); + } + + // Set PLLA and PLLB to 800 MHz for automatic tuning + set_pll(SI5351_PLL_FIXED, SI5351_PLLA); + set_pll(SI5351_PLL_FIXED, SI5351_PLLB); + + // Make PLL to CLK assignments for automatic tuning + // set_ms_source also updates pll_assignment[] + for(i = 0; i < 6; i++) + { + set_ms_source((enum si5351_clock)i, SI5351_PLLA); + } + for(i = 6; i < 8; i++) + { + set_ms_source((enum si5351_clock)i, SI5351_PLLB); + } + + // Turn off spread spectrum (SSC_EN, reg 149 bit 7). Some chips power up + // or are factory-programmed with it enabled, which spreads/shifts the + // output frequency (issues #82, #86). + uint8_t ssc = si5351_read(SI5351_SSC_PARAM0); + si5351_write(SI5351_SSC_PARAM0, ssc & ~SI5351_SSC_EN); + + // Reset the VCXO param + si5351_write(SI5351_VXCO_PARAMETERS_LOW, 0); + si5351_write(SI5351_VXCO_PARAMETERS_MID, 0); + si5351_write(SI5351_VXCO_PARAMETERS_HIGH, 0); + + // Then reset the PLLs + pll_reset(SI5351_PLLA); + pll_reset(SI5351_PLLB); + + // Set initial frequencies + for(i = 0; i < 8; i++) + { + reset_freq((enum si5351_clock)i); + } +} + +/* + * reset_freq(enum si5351_clock clk) + * + * Sets one CLK output back to the initial frequency state: stored + * frequency 0 Hz and the output disabled. Clears the first-set flag so + * the next set_freq() on that output enables it automatically, the same + * as the first call. Other outputs are not modified. Pass a valid + * si5351_clock value (SI5351_CLK0..SI5351_CLK7); out-of-range values + * are not supported. + * + * clk - Clock output + * (use the si5351_clock enum) + */ +void Si5351::reset_freq(enum si5351_clock clk) +{ + if((uint8_t)clk > (uint8_t)SI5351_CLK7) + { + return; + } + + clk_freq[(uint8_t)clk] = 0; + output_enable(clk, 0); + clk_first_set[(uint8_t)clk] = false; +} + +/* + * set_freq(uint64_t freq, enum si5351_clock clk) + * + * Sets the clock frequency of the specified CLK output. + * Frequency range of 4 kHz to 200 MHz on CLK0-CLK5 (out-of-range + * requests are clamped). CLK6/CLK7 are limited to just under 150 MHz. + * + * freq - Output frequency in units of 0.01 Hz (Hz * SI5351_FREQ_MULT), + * e.g. 1400000000ULL = 14 MHz + * clk - Clock output + * (use the si5351_clock enum) + */ +uint8_t Si5351::set_freq(uint64_t freq, enum si5351_clock clk) +{ + struct Si5351RegSet ms_reg; + uint64_t pll_freq; + uint8_t int_mode = 0; + uint8_t div_by_4 = 0; + uint8_t r_div = 0; + + // Check which Multisynth is being set + if((uint8_t)clk <= (uint8_t)SI5351_CLK5) + { + // MS0 through MS5 logic + // --------------------- + + // Lower bounds check + if(freq > 0 && freq < SI5351_CLKOUT_MIN_FREQ * SI5351_FREQ_MULT) + { + freq = SI5351_CLKOUT_MIN_FREQ * SI5351_FREQ_MULT; + } + + // Upper bounds check + if(freq > SI5351_FOUT_MAX * SI5351_FREQ_MULT) + { + freq = SI5351_FOUT_MAX * SI5351_FREQ_MULT; + } + + // Is another output on the same PLL already >100 MHz (and so + // holding the PLL at a frequency chosen for it)? + uint8_t i; + uint8_t pll_held = 0; + for(i = 0; i < 6; i++) + { + if(clk_freq[i] > (SI5351_MULTISYNTH_SHARE_MAX * SI5351_FREQ_MULT) + && i != (uint8_t)clk && pll_assignment[i] == pll_assignment[clk]) + { + pll_held = 1; + } + } + + // A >100 MHz request needs a PLL of its own. If another output holds + // this one above 100 MHz, move this output to the other PLL when no + // other active output uses it (#39); otherwise it is refused below. + if(pll_held && freq > (SI5351_MULTISYNTH_SHARE_MAX * SI5351_FREQ_MULT)) + { + enum si5351_pll other = (pll_assignment[clk] == SI5351_PLLA) ? SI5351_PLLB : SI5351_PLLA; + uint8_t other_used = 0; + for(i = 0; i < 8; i++) + { + if(i != (uint8_t)clk && clk_freq[i] != 0 && pll_assignment[i] == other) + { + other_used = 1; + } + } + if(!other_used) + { + set_ms_source(clk, other); + pll_held = 0; + } + } + + // Enable the output on first set_freq only + // (a >100 MHz request is refused below if the PLL is still held) + if(!(pll_held && freq > (SI5351_MULTISYNTH_SHARE_MAX * SI5351_FREQ_MULT)) + && clk_first_set[(uint8_t)clk] == false) + { + output_enable(clk, 1); + clk_first_set[(uint8_t)clk] = true; + } + + // If requested freq >100 MHz and no other outputs are already >100 MHz, + // we need to recalculate PLLA and then recalculate all other CLK outputs + // on same PLL + if(freq > (SI5351_MULTISYNTH_SHARE_MAX * SI5351_FREQ_MULT)) + { + if(pll_held) + { + return 1; // both PLLs in use and this one is held >100 MHz + } + + // Set the freq in memory + clk_freq[(uint8_t)clk] = freq; + + // Calculate the proper PLL frequency + pll_freq = multisynth_calc(freq, 0, &ms_reg); + } + else + { + clk_freq[(uint8_t)clk] = freq; + + // Select the proper R div value + r_div = select_r_div(&freq, SI5351_CLKOUT_MIN_FREQ); + + pll_freq = (pll_assignment[clk] == SI5351_PLLA) ? plla_freq : pllb_freq; + + // A >100 MHz setting can leave the PLL low (e.g. 620 MHz for 155 MHz), + // making the MS ratio for this output < 8, outside the valid + // fractional range (155 -> 96 MHz gave ~88.5 MHz, issue #65). If no + // other output holds the PLL, re-plan it at the fixed 800 MHz below. + uint8_t replan = 0; + if(pll_freq < freq * 8) + { + replan = 1; + if(pll_held) + { + // The PLL is held above 100 MHz by another output (#65: + // 155 MHz on CLK0, then 99 MHz on CLK2), so move this output + // to the other PLL: reuse it as is if it gives a valid ratio, + // or re-plan it if no other output uses it. Otherwise keep + // the old behaviour. + enum si5351_pll other = (pll_assignment[clk] == SI5351_PLLA) ? SI5351_PLLB : SI5351_PLLA; + uint64_t other_freq = (other == SI5351_PLLA) ? plla_freq : pllb_freq; + uint8_t other_used = 0; + for(i = 0; i < 8; i++) + { + if(i != (uint8_t)clk && clk_freq[i] != 0 && pll_assignment[i] == other) + { + other_used = 1; + } + } + + replan = 0; + if(other_freq >= freq * 8) + { + set_ms_source(clk, other); + pll_freq = other_freq; + } + else if(!other_used) + { + set_ms_source(clk, other); + replan = 1; + } + } + } + + if(!replan) + { + // Calculate the synth parameters + multisynth_calc(freq, pll_freq, &ms_reg); + + // Set multisynth registers + set_ms(clk, ms_reg, int_mode, r_div, div_by_4); + + return 0; + } + + pll_freq = SI5351_PLL_FIXED; + } + + // Set PLL and recalculate every synth on it, this one included + defer_pll_reset = 1; + recalc_force = 1; + set_pll(pll_freq, pll_assignment[clk]); + recalc_force = 0; + + // Reset the PLL (once, after all PLL and MS writes) + defer_pll_reset = 0; + reset_owed = 0; + pll_reset(pll_assignment[clk]); + + return 0; + } + else + { + // MS6 and MS7 logic + // ----------------- + uint8_t peer = (clk == SI5351_CLK6) ? (uint8_t)SI5351_CLK7 : (uint8_t)SI5351_CLK6; + + // There is no valid MS6/7 setting for 0 Hz + if(freq == 0) + { + return 1; + } + + // Lower bounds check (lowest valid MS6/7 ratio, VCO and R combination) + if(freq < SI5351_CLKOUT67_MIN) + { + freq = SI5351_CLKOUT67_MIN; + } + + // Upper bounds check + if(freq >= SI5351_MULTISYNTH_DIVBY4_FREQ * SI5351_FREQ_MULT) + { + freq = SI5351_MULTISYNTH_DIVBY4_FREQ * SI5351_FREQ_MULT - 1; + } + + // If one of CLK6 or CLK7 is already set when trying to set the other, + // we have to ensure that it will also have an even integer division + // ratio (6..254, using the R divider if needed) with the same PLL, + // otherwise do not set it. + if(clk_freq[peer] != 0) + { + ms_reg.p1 = ms67_div(pllb_freq, freq, &r_div); + if(ms_reg.p1 == 0) + { + return 1; + } + ms_reg.p2 = 0; + ms_reg.p3 = 0; + + // Set the freq in memory + clk_freq[(uint8_t)clk] = freq; + } + else + { + // No previous assignment, so set PLLB based on this clock + + // Set the freq in memory + clk_freq[(uint8_t)clk] = freq; + + // Smallest R divider that keeps the MS6/7 ratio <= 254 + while(freq < SI5351_MS67_MIN_FREQ && r_div < SI5351_OUTPUT_CLK_DIV_128) + { + freq <<= 1; + r_div++; + } + + pll_freq = multisynth67_calc(freq, 0, &ms_reg); + // Preserve original asymmetry: CLK6 hardcodes PLLB; CLK7 uses assignment + recalc_skip = clk; + defer_pll_reset = 1; + set_pll(pll_freq, (clk == SI5351_CLK6) ? SI5351_PLLB : pll_assignment[clk]); + recalc_skip = 0xFF; + } + + // Enable the output on first set_freq only, as for CLK0-5 (DEV-14). + // reset() already powers up CLK6/7 (reg 22/23 = 0x0C). + if(clk_first_set[(uint8_t)clk] == false) + { + output_enable(clk, 1); + clk_first_set[(uint8_t)clk] = true; + } + + div_by_4 = 0; + int_mode = 0; + + // Set multisynth registers (MS must be set before PLL) + set_ms(clk, ms_reg, int_mode, r_div, div_by_4); + + // One reset, after all PLL and MS writes, if the recalc owes one + defer_pll_reset = 0; + if(reset_owed) + { + reset_owed = 0; + pll_reset(pll_assignment[clk]); + } + + return 0; + } +} + +/* + * set_freq_manual(uint64_t freq, uint64_t pll_freq, enum si5351_clock clk) + * + * Sets the clock frequency of the specified CLK output using the given PLL + * frequency. You must ensure that the MS is assigned to the correct PLL and + * that the PLL is set to the correct frequency before using this method. + * + * It is important to note that if you use this method, you will have to + * track that all settings are sane yourself. + * + * freq - Output frequency in units of 0.01 Hz (Hz * SI5351_FREQ_MULT) + * pll_freq - Frequency of the PLL driving the Multisynth in units of + * 0.01 Hz (Hz * 100) + * clk - Clock output + * (use the si5351_clock enum) + */ +uint8_t Si5351::set_freq_manual(uint64_t freq, uint64_t pll_freq, enum si5351_clock clk) +{ + struct Si5351RegSet ms_reg; + uint8_t int_mode = 0; + uint8_t div_by_4 = 0; + + // Lower bounds check + if(freq > 0 && freq < SI5351_CLKOUT_MIN_FREQ * SI5351_FREQ_MULT) + { + freq = SI5351_CLKOUT_MIN_FREQ * SI5351_FREQ_MULT; + } + + // Upper bounds check + if(freq > SI5351_FOUT_MAX * SI5351_FREQ_MULT) + { + freq = SI5351_FOUT_MAX * SI5351_FREQ_MULT; + } + + uint8_t r_div = 0; + + // MS6/7 only take even integer ratios 6..254 (AN619): find one with the + // R divider, or refuse before writing anything if there is none + if((uint8_t)clk >= (uint8_t)SI5351_CLK6) + { + ms_reg.p1 = ms67_div(pll_freq, freq, &r_div); + if(ms_reg.p1 == 0) + { + return 1; + } + ms_reg.p2 = 0; + ms_reg.p3 = 0; + } + + else if(freq >= SI5351_MULTISYNTH_DIVBY4_FREQ * SI5351_FREQ_MULT) + { + // DIVBY4 output is fVCO / 4: keep it within the 200 MHz maximum + if(pll_freq > 4 * SI5351_FOUT_MAX * SI5351_FREQ_MULT) + { + pll_freq = 4 * SI5351_FOUT_MAX * SI5351_FREQ_MULT; + } + } + else if(freq == 0 || (pll_freq < 8 * freq && pll_freq != 6 * freq)) + { + // AN619: MS ratios are 4 (DIVBY4), 6, 8 or 8+1/1048575..2048 only. + // Refuse anything else before writing. + return 1; + } + + clk_freq[(uint8_t)clk] = freq; + + recalc_skip = clk; + defer_pll_reset = 1; + set_pll(pll_freq, pll_assignment[clk]); + recalc_skip = 0xFF; + + // Enable the output + output_enable(clk, 1); + + if((uint8_t)clk <= (uint8_t)SI5351_CLK5) + { + // Select the proper R div value + r_div = select_r_div(&freq, SI5351_CLKOUT_MIN_FREQ); + + // Calculate the synth parameters + multisynth_calc(freq, pll_freq, &ms_reg); + + // If freq > 150 MHz, we need to use DIVBY4 and integer mode + if(freq >= SI5351_MULTISYNTH_DIVBY4_FREQ * SI5351_FREQ_MULT) + { + div_by_4 = 1; + int_mode = 1; + } + } + + // Set multisynth registers (MS must be set before PLL) + set_ms(clk, ms_reg, int_mode, r_div, div_by_4); + + // One reset, after all PLL and MS writes, if one is owed + defer_pll_reset = 0; + if(reset_owed) + { + reset_owed = 0; + pll_reset(pll_assignment[clk]); + } + + return 0; +} + +/* + * set_pll(uint64_t pll_freq, enum si5351_pll target_pll) + * + * Set the specified PLL to a specific oscillation frequency + * + * pll_freq - Desired PLL frequency in Hz * 100 + * target_pll - Which PLL to set + * (use the si5351_pll enum) + */ +void Si5351::set_pll(uint64_t pll_freq, enum si5351_pll target_pll) +{ + struct Si5351RegSet pll_reg; + uint8_t params[SI5351_PARAMETERS_LENGTH]; + + // While the VCXO is in use, PLLB must keep c = 10^6 (AN619 sec. 3.2, 7) + if(target_pll == SI5351_PLLB && vcxo_pull) + { + set_vcxo(pll_freq, vcxo_pull); + return; + } + + if(!ref_ok(xtal_freq[(uint8_t)((target_pll == SI5351_PLLA) ? plla_ref_osc : pllb_ref_osc)])) + { + return; + } + + if(target_pll == SI5351_PLLA) + { + pll_calc(SI5351_PLLA, pll_freq, &pll_reg, ref_correction[plla_ref_osc], 0); + } + else + { + pll_calc(SI5351_PLLB, pll_freq, &pll_reg, ref_correction[pllb_ref_osc], 0); + } + + // Derive the register values to write + pack_reg_set(&pll_reg, params, 0); + + // Write the parameters + si5351_write_bulk((target_pll == SI5351_PLLA) ? SI5351_PLLA_PARAMETERS : SI5351_PLLB_PARAMETERS, + SI5351_PARAMETERS_LENGTH, params); + pll_update(target_pll, pll_freq); +} + +/* + * pll_update(enum si5351_pll pll, uint64_t pll_freq) + * + * Store a newly programmed PLL frequency. If it changed (and is inside the + * VCO range, so pll_calc() did not clamp it), rewrite the MS (and R, DIVBY4, + * integer bits) of every active output on this PLL for its stored + * frequency, so outputs keep their frequency when the PLL changes. An output with no valid ratio on the new PLL (AN619: + * 4 via DIVBY4, 6, 8+; MS6/7 even 6..254) is left as it is. Any reset needed + * for a DIVBY4 exit is issued once at the end, or by the caller if it is + * deferring resets. + */ +void Si5351::pll_update(enum si5351_pll pll, uint64_t pll_freq) +{ + uint8_t i, r_div, div_by_4, outer = defer_pll_reset; + uint64_t *stored = pll ? &pllb_freq : &plla_freq; + struct Si5351RegSet reg; + + i = (*stored != pll_freq) && pll_freq >= SI5351_PLL_VCO_MIN * SI5351_FREQ_MULT + && pll_freq <= SI5351_PLL_VCO_MAX * SI5351_FREQ_MULT; + *stored = pll_freq; + if(!i && !recalc_force) + { + return; + } + + defer_pll_reset = 1; + for(i = 0; i < 8; i++) + { + uint64_t f = clk_freq[i]; + if(f == 0 || i == recalc_skip || pll_assignment[i] != pll) + { + continue; + } + r_div = 0; + if(i >= 6) + { + reg.p1 = ms67_div(pll_freq, f, &r_div); + if(reg.p1 == 0) + { + continue; + } + reg.p2 = 0; + reg.p3 = 0; + div_by_4 = 0; + } + else + { + r_div = select_r_div(&f, SI5351_CLKOUT_MIN_FREQ); + div_by_4 = (f >= SI5351_MULTISYNTH_DIVBY4_FREQ * SI5351_FREQ_MULT); + uint64_t f4 = f << 2; + if(div_by_4 ? (pll_freq != f4) : (pll_freq < (f4 << 1) && pll_freq != f4 + (f << 1))) + { + continue; + } + multisynth_calc(f, pll_freq, ®); + } + set_ms((enum si5351_clock)i, reg, div_by_4, r_div, div_by_4); + } + defer_pll_reset = outer; + if(reset_owed && !outer) + { + reset_owed = 0; + pll_reset(pll); + } +} + +/* + * set_ms(enum si5351_clock clk, struct Si5351RegSet ms_reg, uint8_t int_mode, uint8_t r_div, uint8_t div_by_4) + * + * Set the specified multisynth parameters. Not normally needed, but public for advanced users. + * + * clk - Clock output + * (use the si5351_clock enum) + * int_mode - Set integer mode + * Set to 1 to enable, 0 to disable + * r_div - Desired r_div ratio + * div_by_4 - Set Divide By 4 mode + * Set to 1 to enable, 0 to disable + */ +void Si5351::set_ms(enum si5351_clock clk, struct Si5351RegSet ms_reg, uint8_t int_mode, uint8_t r_div, uint8_t div_by_4) +{ + uint8_t params[SI5351_PARAMETERS_LENGTH]; + + if((uint8_t)clk <= (uint8_t)SI5351_CLK5) + { + // Preserve non-p1 bits in the p1[17:16] parameter byte + uint8_t reg_val = si5351_read((SI5351_CLK0_PARAMETERS + 2) + ((uint8_t)clk * 8)); + // MS currently in DIVBY4 mode and being switched out of it? (#65) + uint8_t leaving_divby4 = ((reg_val & SI5351_OUTPUT_CLK_DIVBY4) == SI5351_OUTPUT_CLK_DIVBY4) && !div_by_4; + reg_val &= ~(0x03); + pack_reg_set(&ms_reg, params, reg_val); + + // CLK0..CLK5 parameter blocks are linear: base + clk*8 + si5351_write_bulk(SI5351_CLK0_PARAMETERS + ((uint8_t)clk * 8), + SI5351_PARAMETERS_LENGTH, params); + set_int(clk, int_mode); + ms_div(clk, r_div, div_by_4); + + // A Multisynth leaving DIVBY4 mode produces no output until its PLL is + // reset (issue #65, e.g. 156 MHz -> 80 MHz). Reset only on that + // transition so normal tuning stays glitch-free. reset() leaves the + // DIVBY4 bits set, so this also covers reset() followed by set_freq(). + reset_owed |= leaving_divby4 && defer_pll_reset; + if(leaving_divby4 && !defer_pll_reset) + { + pll_reset(pll_assignment[clk]); + } + } + else if((uint8_t)clk <= (uint8_t)SI5351_CLK7) + { + // MS6 and MS7 only use one register (90 / 91). + // Bounded to CLK7 so an out-of-range clk writes nothing, as the + // original switch() did (otherwise clk=8 would clobber reg 92). + si5351_write(SI5351_CLK6_PARAMETERS + ((uint8_t)clk - (uint8_t)SI5351_CLK6), + (uint8_t)ms_reg.p1); + ms_div(clk, r_div, div_by_4); + } +} + +/* + * output_enable(enum si5351_clock clk, uint8_t enable) + * + * Enable or disable a chosen output + * clk - Clock output + * (use the si5351_clock enum) + * enable - Set to 1 to enable, 0 to disable + */ +void Si5351::output_enable(enum si5351_clock clk, uint8_t enable) +{ + uint8_t reg_val; + + reg_val = si5351_read(SI5351_OUTPUT_ENABLE_CTRL); + + if(enable == 1) + { + reg_val &= ~(1<<(uint8_t)clk); + } + else + { + reg_val |= (1<<(uint8_t)clk); + } + + si5351_write(SI5351_OUTPUT_ENABLE_CTRL, reg_val); +} + +/* + * drive_strength(enum si5351_clock clk, enum si5351_drive drive) + * + * Sets the drive strength of the specified clock output + * + * clk - Clock output + * (use the si5351_clock enum) + * drive - Desired drive level + * (use the si5351_drive enum) + */ +void Si5351::drive_strength(enum si5351_clock clk, enum si5351_drive drive) +{ + uint8_t reg_val; + const uint8_t mask = 0x03; + + reg_val = si5351_read(SI5351_CLK0_CTRL + (uint8_t)clk); + reg_val &= ~(mask); + + switch(drive) + { + case SI5351_DRIVE_2MA: + reg_val |= 0x00; + break; + case SI5351_DRIVE_4MA: + reg_val |= 0x01; + break; + case SI5351_DRIVE_6MA: + reg_val |= 0x02; + break; + case SI5351_DRIVE_8MA: + reg_val |= 0x03; + break; + default: + break; + } + + si5351_write(SI5351_CLK0_CTRL + (uint8_t)clk, reg_val); +} + +/* + * update_status(void) + * + * Call this to update the status structs, then access them + * via the dev_status and dev_int_status global members. + * + * See the header file for the struct definitions. These + * correspond to the flag names for registers 0 and 1 in + * the Si5351 datasheet. + */ +void Si5351::update_status(void) +{ + update_sys_status(&dev_status); + update_int_status(&dev_int_status); +} + +/* + * set_correction(int32_t corr, enum si5351_pll_input ref_osc) + * + * corr - Correction factor in ppb + * ref_osc - Desired reference oscillator + * (use the si5351_pll_input enum) + * + * Use this to set the oscillator correction factor. + * This value is a signed 32-bit integer of the + * parts-per-billion value that the actual oscillation + * frequency deviates from the specified frequency. + * + * The frequency calibration is done as a one-time procedure. + * Any desired test frequency within the normal range of the + * Si5351 should be set (with corr = 0), then the actual output + * frequency should be measured as accurately as possible. Then: + * + * corr (ppb) = (f_measured - f_set) / f_set * 1e9 + * + * with both frequencies in Hz. The Hz error scales with the test + * frequency: 1 Hz of error is 10 ppb at 100 MHz but 100 ppb at + * 10 MHz. + * + * Sign convention: a positive corr means the output (and so the + * reference) runs high. The library then treats the reference as + * ref * (1 + corr / 1e9) when computing the PLL, which lowers the + * output back to the set frequency; a negative corr raises it. + * + * Since the Si5351 itself has an intrinsic 0 PPM error, this + * correction factor is good across the entire tuning range of + * the Si5351. Once this calibration is done accurately, it + * should not have to be done again for the same Si5351 and + * crystal. + */ +void Si5351::set_correction(int32_t corr, enum si5351_pll_input ref_osc) +{ + ref_correction[(uint8_t)ref_osc] = corr; + + // Recalculate and set PLL freqs based on correction value + set_pll(plla_freq, SI5351_PLLA); + set_pll(pllb_freq, SI5351_PLLB); +} + +/* + * set_phase(enum si5351_clock clk, uint8_t phase) + * + * clk - Clock output + * (use the si5351_clock enum) + * phase - 7-bit phase word + * (in units of VCO/4 period) + * + * Write the 7-bit phase register. This must be used + * with a user-set PLL frequency so that the user can + * calculate the proper tuning word based on the PLL period. + */ +void Si5351::set_phase(enum si5351_clock clk, uint8_t phase) +{ + // Only CLK0-5 have a phase offset register (165-170); 171+ are reserved + if((uint8_t)clk > (uint8_t)SI5351_CLK5) + { + return; + } + + // Mask off the upper bit since it is reserved + phase = phase & 0b01111111; + + si5351_write(SI5351_CLK0_PHASE_OFFSET + (uint8_t)clk, phase); +} + +/* + * get_correction(enum si5351_pll_input ref_osc) + * + * ref_osc - Desired reference oscillator + * 0: crystal oscillator (XO) + * 1: external clock input (CLKIN) + * + * Returns the oscillator correction factor stored + * in RAM. + */ +int32_t Si5351::get_correction(enum si5351_pll_input ref_osc) +{ + return ref_correction[(uint8_t)ref_osc]; +} + +/* + * pll_reset(enum si5351_pll target_pll) + * + * target_pll - Which PLL to reset + * (use the si5351_pll enum) + * + * Apply a reset to the indicated PLL. + */ +void Si5351::pll_reset(enum si5351_pll target_pll) +{ + if(target_pll == SI5351_PLLA) + { + si5351_write(SI5351_PLL_RESET, SI5351_PLL_RESET_A); + } + else if(target_pll == SI5351_PLLB) + { + si5351_write(SI5351_PLL_RESET, SI5351_PLL_RESET_B); + } +} + +/* + * set_ms_source(enum si5351_clock clk, enum si5351_pll pll) + * + * clk - Clock output + * (use the si5351_clock enum) + * pll - Which PLL to use as the source + * (use the si5351_pll enum) + * + * Set the desired PLL source for a multisynth. + */ +void Si5351::set_ms_source(enum si5351_clock clk, enum si5351_pll pll) +{ + uint8_t reg_val; + + reg_val = si5351_read(SI5351_CLK0_CTRL + (uint8_t)clk); + + if(pll == SI5351_PLLA) + { + reg_val &= ~(SI5351_CLK_PLL_SELECT); + } + else if(pll == SI5351_PLLB) + { + reg_val |= SI5351_CLK_PLL_SELECT; + } + + si5351_write(SI5351_CLK0_CTRL + (uint8_t)clk, reg_val); + + pll_assignment[(uint8_t)clk] = pll; +} + +/* + * set_int(enum si5351_clock clk, uint8_t int_mode) + * + * clk - Clock output + * (use the si5351_clock enum) + * enable - Set to 1 to enable, 0 to disable + * + * Set the indicated multisynth into integer mode. + */ +void Si5351::set_int(enum si5351_clock clk, uint8_t enable) +{ + uint8_t reg_val; + + // MS6/7 are always integer and have no MSx_INT bit: bit 6 of reg 22/23 + // is FBA_INT/FBB_INT (PLLA/PLLB feedback integer mode), so do nothing + if((uint8_t)clk > (uint8_t)SI5351_CLK5) + { + return; + } + reg_val = si5351_read(SI5351_CLK0_CTRL + (uint8_t)clk); + + if(enable == 1) + { + reg_val |= (SI5351_CLK_INTEGER_MODE); + } + else + { + reg_val &= ~(SI5351_CLK_INTEGER_MODE); + } + + si5351_write(SI5351_CLK0_CTRL + (uint8_t)clk, reg_val); + + // Integer mode indication + /* + switch(clk) + { + case SI5351_CLK0: + clk0_int_mode = enable; + break; + case SI5351_CLK1: + clk1_int_mode = enable; + break; + case SI5351_CLK2: + clk2_int_mode = enable; + break; + default: + break; + } + */ +} + +/* + * set_clock_pwr(enum si5351_clock clk, uint8_t pwr) + * + * clk - Clock output + * (use the si5351_clock enum) + * pwr - Set to 1 to enable, 0 to disable + * + * Enable or disable power to a clock output (a power + * saving feature). + */ +void Si5351::set_clock_pwr(enum si5351_clock clk, uint8_t pwr) +{ + uint8_t reg_val; //, reg; + reg_val = si5351_read(SI5351_CLK0_CTRL + (uint8_t)clk); + + if(pwr == 1) + { + reg_val &= 0b01111111; + } + else + { + reg_val |= 0b10000000; + } + + si5351_write(SI5351_CLK0_CTRL + (uint8_t)clk, reg_val); +} + +/* + * set_clock_invert(enum si5351_clock clk, uint8_t inv) + * + * clk - Clock output + * (use the si5351_clock enum) + * inv - Set to 1 to enable, 0 to disable + * + * Enable to invert the clock output waveform. + */ +void Si5351::set_clock_invert(enum si5351_clock clk, uint8_t inv) +{ + uint8_t reg_val; + reg_val = si5351_read(SI5351_CLK0_CTRL + (uint8_t)clk); + + if(inv == 1) + { + reg_val |= (SI5351_CLK_INVERT); + } + else + { + reg_val &= ~(SI5351_CLK_INVERT); + } + + si5351_write(SI5351_CLK0_CTRL + (uint8_t)clk, reg_val); +} + +/* + * set_clock_source(enum si5351_clock clk, enum si5351_clock_source src) + * + * clk - Clock output + * (use the si5351_clock enum) + * src - Which clock source to use for the multisynth + * (use the si5351_clock_source enum) + * + * Set the clock source for a multisynth (based on the options + * presented for Registers 16-23 in the Silicon Labs AN619 document). + * Choices are XTAL, CLKIN, MS0, or the multisynth associated with + * the clock output. + */ +void Si5351::set_clock_source(enum si5351_clock clk, enum si5351_clock_source src) +{ + uint8_t reg_val; + reg_val = si5351_read(SI5351_CLK0_CTRL + (uint8_t)clk); + + // Clear the bits first + reg_val &= ~(SI5351_CLK_INPUT_MASK); + + switch(src) + { + case SI5351_CLK_SRC_XTAL: + reg_val |= (SI5351_CLK_INPUT_XTAL); + break; + case SI5351_CLK_SRC_CLKIN: + reg_val |= (SI5351_CLK_INPUT_CLKIN); + break; + case SI5351_CLK_SRC_MS0: + if(clk == SI5351_CLK0) + { + return; + } + + reg_val |= (SI5351_CLK_INPUT_MULTISYNTH_0_4); + break; + case SI5351_CLK_SRC_MS: + reg_val |= (SI5351_CLK_INPUT_MULTISYNTH_N); + break; + default: + return; + } + + si5351_write(SI5351_CLK0_CTRL + (uint8_t)clk, reg_val); +} + +/* + * set_clock_disable(enum si5351_clock clk, enum si5351_clock_disable dis_state) + * + * clk - Clock output + * (use the si5351_clock enum) + * dis_state - Desired state of the output upon disable + * (use the si5351_clock_disable enum) + * + * Set the state of the clock output when it is disabled. Per page 27 + * of AN619 (Registers 24 and 25), there are four possible values: low, + * high, high impedance, and never disabled. + */ +void Si5351::set_clock_disable(enum si5351_clock clk, enum si5351_clock_disable dis_state) +{ + uint8_t reg_val, reg; + + if (clk <= SI5351_CLK3) + { + reg = SI5351_CLK3_0_DISABLE_STATE; + } + else if(clk >= SI5351_CLK4 && clk <= SI5351_CLK7) + { + reg = SI5351_CLK7_4_DISABLE_STATE; + } + else return; + + reg_val = si5351_read(reg); + + if (clk <= SI5351_CLK3) + { + reg_val &= ~(0b11 << (clk * 2)); + reg_val |= dis_state << (clk * 2); + } + else if(clk >= SI5351_CLK4 && clk <= SI5351_CLK7) + { + reg_val &= ~(0b11 << ((clk - 4) * 2)); + reg_val |= dis_state << ((clk - 4) * 2); + } + + si5351_write(reg, reg_val); +} + +/* + * set_clock_fanout(enum si5351_clock_fanout fanout, uint8_t enable) + * + * fanout - Desired clock fanout + * (use the si5351_clock_fanout enum) + * enable - Set to 1 to enable, 0 to disable + * + * Use this function to enable or disable the clock fanout options + * for individual clock outputs. If you intend to output the XO or + * CLKIN on the clock outputs, enable this first. + * + * By default, only the Multisynth fanout is enabled at startup. + */ +void Si5351::set_clock_fanout(enum si5351_clock_fanout fanout, uint8_t enable) +{ + uint8_t reg_val; + reg_val = si5351_read(SI5351_FANOUT_ENABLE); + + switch(fanout) + { + case SI5351_FANOUT_CLKIN: + if(enable) + { + reg_val |= SI5351_CLKIN_ENABLE; + } + else + { + reg_val &= ~(SI5351_CLKIN_ENABLE); + } + break; + case SI5351_FANOUT_XO: + if(enable) + { + reg_val |= SI5351_XTAL_ENABLE; + } + else + { + reg_val &= ~(SI5351_XTAL_ENABLE); + } + break; + case SI5351_FANOUT_MS: + if(enable) + { + reg_val |= SI5351_MULTISYNTH_ENABLE; + } + else + { + reg_val &= ~(SI5351_MULTISYNTH_ENABLE); + } + break; + } + + si5351_write(SI5351_FANOUT_ENABLE, reg_val); +} + +/* + * set_pll_input(enum si5351_pll pll, enum si5351_pll_input input) + * + * pll - Which PLL to use as the source + * (use the si5351_pll enum) + * input - Which reference oscillator to use as PLL input + * (use the si5351_pll_input enum) + * + * Set the desired reference oscillator source for the given PLL. + */ +void Si5351::set_pll_input(enum si5351_pll pll, enum si5351_pll_input input) +{ + uint8_t reg_val; + reg_val = si5351_read(SI5351_PLL_INPUT_SOURCE); + + // CLKIN_DIV[1:0] is a 2-bit field shared by both PLLs: replace it (not + // OR into it) when a PLL is switched to CLKIN, leave it alone for XO + + switch(pll) + { + case SI5351_PLLA: + if(input == SI5351_PLL_INPUT_CLKIN) + { + reg_val |= SI5351_PLLA_SOURCE; + reg_val = (reg_val & ~(SI5351_CLKIN_DIV_MASK)) | clkin_div; + plla_ref_osc = SI5351_PLL_INPUT_CLKIN; + } + else + { + reg_val &= ~(SI5351_PLLA_SOURCE); + plla_ref_osc = SI5351_PLL_INPUT_XO; + } + break; + case SI5351_PLLB: + if(input == SI5351_PLL_INPUT_CLKIN) + { + reg_val |= SI5351_PLLB_SOURCE; + reg_val = (reg_val & ~(SI5351_CLKIN_DIV_MASK)) | clkin_div; + pllb_ref_osc = SI5351_PLL_INPUT_CLKIN; + } + else + { + reg_val &= ~(SI5351_PLLB_SOURCE); + pllb_ref_osc = SI5351_PLL_INPUT_XO; + } + break; + default: + return; + } + + si5351_write(SI5351_PLL_INPUT_SOURCE, reg_val); + + set_pll(plla_freq, SI5351_PLLA); + set_pll(pllb_freq, SI5351_PLLB); +} + +/* + * set_vcxo(uint64_t pll_freq, uint8_t ppm) + * + * pll_freq - Desired PLL base frequency in Hz * 100 + * ppm - VCXO pull limit in ppm + * + * Set the parameters for the VCXO on the Si5351B. + */ +void Si5351::set_vcxo(uint64_t pll_freq, uint8_t ppm) +{ + struct Si5351RegSet pll_reg; + uint64_t vcxo_param; + + // Bounds check + if(ppm < SI5351_VCXO_PULL_MIN) + { + ppm = SI5351_VCXO_PULL_MIN; + } + + if(ppm > SI5351_VCXO_PULL_MAX) + { + ppm = SI5351_VCXO_PULL_MAX; + } + + if(!ref_ok(xtal_freq[(uint8_t)pllb_ref_osc])) + { + return; + } + + // Remember the VCXO setting so later PLLB updates keep c = 10^6 + vcxo_pull = ppm; + + // Set PLLB params + vcxo_param = pll_calc(SI5351_PLLB, pll_freq, &pll_reg, ref_correction[pllb_ref_osc], 1); + + // Derive the register values to write + uint8_t params[SI5351_PARAMETERS_LENGTH]; + uint8_t temp; + + pack_reg_set(&pll_reg, params, 0); + + // Write the parameters + si5351_write_bulk(SI5351_PLLB_PARAMETERS, SI5351_PARAMETERS_LENGTH, params); + + // Write the VCXO parameters + vcxo_param = ((vcxo_param * ppm * SI5351_VCXO_MARGIN) / 100ULL) / 1000000ULL; + + temp = (uint8_t)(vcxo_param & 0xFF); + si5351_write(SI5351_VXCO_PARAMETERS_LOW, temp); + + temp = (uint8_t)((vcxo_param >> 8) & 0xFF); + si5351_write(SI5351_VXCO_PARAMETERS_MID, temp); + + temp = (uint8_t)((vcxo_param >> 16) & 0x3F); + si5351_write(SI5351_VXCO_PARAMETERS_HIGH, temp); + + pll_update(SI5351_PLLB, pll_freq); +} + +/* + * set_ref_freq(uint32_t ref_freq, enum si5351_pll_input ref_osc) + * + * ref_freq - Reference oscillator frequency in Hz + * ref_osc - Which reference oscillator frequency to set + * (use the si5351_pll_input enum) + * + * Set the reference frequency value for the desired reference oscillator + */ +void Si5351::set_ref_freq(uint32_t ref_freq, enum si5351_pll_input ref_osc) +{ + // uint8_t reg_val; + //reg_val = si5351_read(SI5351_PLL_INPUT_SOURCE); + + // Clear the bits first + //reg_val &= ~(SI5351_CLKIN_DIV_MASK); + + if(ref_freq <= 30000000UL) + { + xtal_freq[(uint8_t)ref_osc] = ref_freq; + //reg_val |= SI5351_CLKIN_DIV_1; + if(ref_osc == SI5351_PLL_INPUT_CLKIN) + { + clkin_div = SI5351_CLKIN_DIV_1; + } + } + else if(ref_freq > 30000000UL && ref_freq <= 60000000UL) + { + xtal_freq[(uint8_t)ref_osc] = ref_freq / 2; + //reg_val |= SI5351_CLKIN_DIV_2; + if(ref_osc == SI5351_PLL_INPUT_CLKIN) + { + clkin_div = SI5351_CLKIN_DIV_2; + } + } + else if(ref_freq > 60000000UL && ref_freq <= 100000000UL) + { + xtal_freq[(uint8_t)ref_osc] = ref_freq / 4; + //reg_val |= SI5351_CLKIN_DIV_4; + if(ref_osc == SI5351_PLL_INPUT_CLKIN) + { + clkin_div = SI5351_CLKIN_DIV_4; + } + } + else + { + //reg_val |= SI5351_CLKIN_DIV_1; + } + + //si5351_write(SI5351_PLL_INPUT_SOURCE, reg_val); +} + +uint8_t Si5351::si5351_write_bulk(uint8_t addr, uint8_t bytes, uint8_t *data) +{ + _wire->beginTransmission(i2c_bus_addr); + _wire->write(addr); + for(int i = 0; i < bytes; i++) + { + _wire->write(data[i]); + } + uint8_t err = _wire->endTransmission(); + if(err) i2c_error = err; + return err; + +} + +uint8_t Si5351::si5351_write(uint8_t addr, uint8_t data) +{ + _wire->beginTransmission(i2c_bus_addr); + _wire->write(addr); + _wire->write(data); + uint8_t err = _wire->endTransmission(); + if(err) i2c_error = err; + return err; +} + +uint8_t Si5351::si5351_read(uint8_t addr) +{ + uint8_t reg_val = 0; + + _wire->beginTransmission(i2c_bus_addr); + _wire->write(addr); + uint8_t err = _wire->endTransmission(); + if(err) + { + // Address/register write not ACKed: don't read, return 0 + i2c_error = err; + return 0; + } + + if(_wire->requestFrom(i2c_bus_addr, (uint8_t)1) == 0) + { + i2c_error = SI5351_I2C_ERR_NO_DATA; // no data returned + return 0; + } + + while(_wire->available()) + { + reg_val = _wire->read(); + } + + return reg_val; +} + +/* + * get_i2c_error(void) + * + * Returns the most recent I2C error seen by si5351_read(), si5351_write() + * or si5351_write_bulk() since the last call, and clears it. 0 means no + * error. Nonzero values are the Wire endTransmission() codes (1-5, + * core-dependent; also recorded when init() finds no device), or + * SI5351_I2C_ERR_NO_DATA (0xFF) when a read returned no data. Use it after + * set_freq(), update_status() etc. to detect a missing or failing + * device (issue #74); those calls keep their existing return values. + */ +uint8_t Si5351::get_i2c_error(void) +{ + uint8_t err = i2c_error; + i2c_error = 0; + return err; +} + +/*********************/ +/* Private functions */ +/*********************/ + +uint64_t Si5351::pll_calc(enum si5351_pll pll, uint64_t freq, struct Si5351RegSet *reg, int32_t correction, uint8_t vcxo) +{ + uint64_t ref_freq; + if(pll == SI5351_PLLA) + { + ref_freq = xtal_freq[(uint8_t)plla_ref_osc] * SI5351_FREQ_MULT; + } + else + { + ref_freq = xtal_freq[(uint8_t)pllb_ref_osc] * SI5351_FREQ_MULT; + } + //ref_freq = 15974400ULL * SI5351_FREQ_MULT; + uint32_t a, b, c; + uint64_t lltmp; //, denom; + + // Factor calibration value into nominal crystal frequency + // Measured in parts-per-billion + + ref_freq = ref_freq + (int32_t)((((((int64_t)correction) << 31) / 1000000000LL) * ref_freq) >> 31); + + // PLL bounds checking + if (freq < SI5351_PLL_VCO_MIN * SI5351_FREQ_MULT) + { + freq = SI5351_PLL_VCO_MIN * SI5351_FREQ_MULT; + } + if (freq > SI5351_PLL_VCO_MAX * SI5351_FREQ_MULT) + { + freq = SI5351_PLL_VCO_MAX * SI5351_FREQ_MULT; + } + + // Determine integer part of feedback equation + a = freq / ref_freq; + + if (a < SI5351_PLL_A_MIN) + { + a = SI5351_PLL_A_MIN; + freq = ref_freq * SI5351_PLL_A_MIN; + } + if (a > SI5351_PLL_A_MAX) + { + a = SI5351_PLL_A_MAX; + freq = ref_freq * SI5351_PLL_A_MAX; + } + + // Find best approximation for b/c = fVCO mod fIN + // denom = 1000ULL * 1000ULL; + // lltmp = freq % ref_freq; + // lltmp *= denom; + // do_div(lltmp, ref_freq); + + //b = (((uint64_t)(freq % ref_freq)) * RFRAC_DENOM) / ref_freq; + if(vcxo) + { + b = (((uint64_t)(freq % ref_freq)) * 1000000ULL) / ref_freq; + c = 1000000ULL; + } + else + { + b = (((uint64_t)(freq % ref_freq)) * RFRAC_DENOM) / ref_freq; + c = b ? RFRAC_DENOM : 1; + } + + // Calculate parameters + frac_to_reg(a, b, c, reg); + + // Recalculate frequency as fIN * (a + b/c) + lltmp = ref_freq; + lltmp *= b; + do_div(lltmp, c); + freq = lltmp; + freq += ref_freq * a; + + if(vcxo) + { + return (uint64_t)(128 * a * 1000000ULL + b); + } + else + { + return freq; + } +} + +uint64_t Si5351::multisynth_calc(uint64_t freq, uint64_t pll_freq, struct Si5351RegSet *reg) +{ + uint64_t lltmp; + uint32_t a, b, c; + uint8_t divby4 = 0; + uint8_t ret_val = 0; + + // Multisynth bounds checking + if (freq > SI5351_FOUT_MAX * SI5351_FREQ_MULT) + { + freq = SI5351_FOUT_MAX * SI5351_FREQ_MULT; + } + if (freq < SI5351_MULTISYNTH_MIN_FREQ * SI5351_FREQ_MULT) + { + freq = SI5351_MULTISYNTH_MIN_FREQ * SI5351_FREQ_MULT; + } + + if (freq >= SI5351_MULTISYNTH_DIVBY4_FREQ * SI5351_FREQ_MULT) + { + divby4 = 1; + } + + if(pll_freq == 0) + { + // Find largest integer divider for max + // VCO frequency and given target frequency + if(divby4 == 0) + { + lltmp = SI5351_PLL_VCO_MAX * SI5351_FREQ_MULT; // margin needed? + do_div(lltmp, freq); + if(lltmp == 5) + { + lltmp = 4; + } + else if(lltmp == 7) + { + lltmp = 6; + } + a = (uint32_t)lltmp; + } + else + { + a = 4; + } + + b = 0; + c = 1; + pll_freq = a * freq; + } + else + { + // Preset PLL, so return the actual freq for these params instead of PLL freq + ret_val = 1; + + // Determine integer part of feedback equation + a = pll_freq / freq; + + if (a < SI5351_MULTISYNTH_A_MIN) + { + // Clamp to ratio 6; a must follow or ratio 5 is written (DEV-05) + a = SI5351_MULTISYNTH_A_MIN; + freq = pll_freq / SI5351_MULTISYNTH_A_MIN; + } + if (a > SI5351_MULTISYNTH_A_MAX) + { + freq = pll_freq / SI5351_MULTISYNTH_A_MAX; + } + + b = (pll_freq % freq * RFRAC_DENOM) / freq; + c = b ? RFRAC_DENOM : 1; + } + + // Calculate parameters + if (divby4 == 1) + { + reg->p1 = 0; + reg->p2 = 0; + reg->p3 = 1; + } + else + { + frac_to_reg(a, b, c, reg); + } + + if(ret_val == 0) + { + return pll_freq; + } + else + { + return freq; + } +} + +uint64_t Si5351::multisynth67_calc(uint64_t freq, uint64_t pll_freq, struct Si5351RegSet *reg) +{ + //uint8_t p1; + // uint8_t ret_val = 0; + uint32_t a; + uint64_t lltmp; + + // Multisynth bounds checking + if(freq > SI5351_MULTISYNTH67_MAX_FREQ * SI5351_FREQ_MULT) + { + freq = SI5351_MULTISYNTH67_MAX_FREQ * SI5351_FREQ_MULT; + } + if(freq < SI5351_MULTISYNTH_MIN_FREQ * SI5351_FREQ_MULT) + { + freq = SI5351_MULTISYNTH_MIN_FREQ * SI5351_FREQ_MULT; + } + + if(pll_freq == 0) + { + // Find largest integer divider for max + // VCO frequency and given target frequency + lltmp = (SI5351_PLL_VCO_MAX * SI5351_FREQ_MULT) - 100000000UL; // margin needed? + do_div(lltmp, freq); + a = (uint32_t)lltmp; + + // Divisor has to be even + if(a % 2 != 0) + { + a++; + } + + // Divisor bounds check + if(a < SI5351_MULTISYNTH_A_MIN) + { + a = SI5351_MULTISYNTH_A_MIN; + } + if(a > SI5351_MULTISYNTH67_A_MAX) + { + a = SI5351_MULTISYNTH67_A_MAX; + } + + pll_freq = a * freq; + + // PLL bounds checking + if(pll_freq > (SI5351_PLL_VCO_MAX * SI5351_FREQ_MULT)) + { + a -= 2; + pll_freq = a * freq; + } + else if(pll_freq < (SI5351_PLL_VCO_MIN * SI5351_FREQ_MULT)) + { + a += 2; + pll_freq = a * freq; + } + + reg->p1 = (uint8_t)a; + reg->p2 = 0; + reg->p3 = 0; + return pll_freq; + } + else + { + // Multisynth frequency must be integer division of PLL + if(pll_freq % freq) + { + // No good + return 0; + } + else + { + a = pll_freq / freq; + + // Division ratio bounds check + if(a < SI5351_MULTISYNTH_A_MIN || a > SI5351_MULTISYNTH67_A_MAX) + { + // No bueno + return 0; + } + else + { + reg->p1 = (uint8_t)a; + reg->p2 = 0; + reg->p3 = 0; + return 1; + } + } + } +} + +void Si5351::update_sys_status(struct Si5351Status *status) +{ + uint8_t reg_val = 0; + + reg_val = si5351_read(SI5351_DEVICE_STATUS); + + // Parse the register + status->SYS_INIT = (reg_val >> 7) & 0x01; + status->LOL_B = (reg_val >> 6) & 0x01; + status->LOL_A = (reg_val >> 5) & 0x01; + // Bit 4 is LOS_CLKIN and bit 3 is LOS_XTAL (AN619 reg 0). LOS used to + // report LOS_CLKIN, which is always 1 on a crystal-only board (#85). + status->LOS_CLKIN = (reg_val >> 4) & 0x01; + status->LOS_XTAL = (reg_val >> 3) & 0x01; + status->LOS = status->LOS_XTAL; + status->REVID = reg_val & 0x03; +} + +void Si5351::update_int_status(struct Si5351IntStatus *int_status) +{ + uint8_t reg_val = 0; + + reg_val = si5351_read(SI5351_INTERRUPT_STATUS); + + // Parse the register + int_status->SYS_INIT_STKY = (reg_val >> 7) & 0x01; + int_status->LOL_B_STKY = (reg_val >> 6) & 0x01; + int_status->LOL_A_STKY = (reg_val >> 5) & 0x01; + int_status->LOS_CLKIN_STKY = (reg_val >> 4) & 0x01; + int_status->LOS_XTAL_STKY = (reg_val >> 3) & 0x01; + int_status->LOS_STKY = int_status->LOS_XTAL_STKY; +} + +void Si5351::ms_div(enum si5351_clock clk, uint8_t r_div, uint8_t div_by_4) +{ + uint8_t reg_val = 0; + uint8_t reg_addr = 0; + + if((uint8_t)clk <= (uint8_t)SI5351_CLK5) + { + // CLK0..CLK5: parameter byte 2 is linear with clk + reg_addr = SI5351_CLK0_PARAMETERS + 2 + ((uint8_t)clk * 8); + } + else + { + reg_addr = SI5351_CLK6_7_OUTPUT_DIVIDER; + } + + reg_val = si5351_read(reg_addr); + + if((uint8_t)clk <= (uint8_t)SI5351_CLK5) + { + // Clear the relevant bits + reg_val &= ~(0x7c); + + if(div_by_4 == 0) + { + reg_val &= ~(SI5351_OUTPUT_CLK_DIVBY4); + } + else + { + reg_val |= (SI5351_OUTPUT_CLK_DIVBY4); + } + + reg_val |= (r_div << SI5351_OUTPUT_CLK_DIV_SHIFT); + } + else if(clk == SI5351_CLK6) + { + // Clear the relevant bits + reg_val &= ~(0x07); + + reg_val |= r_div; + } + else if(clk == SI5351_CLK7) + { + // Clear the relevant bits + reg_val &= ~(0x70); + + reg_val |= (r_div << SI5351_OUTPUT_CLK_DIV_SHIFT); + } + + si5351_write(reg_addr, reg_val); +} + +uint8_t Si5351::select_r_div(uint64_t *freq, uint32_t min_freq) +{ + /* CLK0-5 output R divider (callers pass min_freq = CLKOUT_MIN_FREQ). + * AN619 Register 44 R0_DIV[2:0], same encoding on R1-R5: + * 000 /1, 001 /2, 010 /4, 011 /8, 100 /16, 101 /32, 110 /64, 111 /128. + * One octave per code. Floor F = CLKOUT_MIN_FREQ, low edge inclusive: + * [F, 2F) /128, [2F, 4F) /64, [4F, 8F) /32, [8F, 16F) /16, + * [16F, 32F) /8, [32F, 64F) /4, [64F, 128F) /2, and /1 from 128F up. + * Divide-by-64 is only [2F, 4F). The next band is divide-by-32 + * (issue #89). For F = 4 kHz the multisynth lands in + * [512 kHz, 1.024 MHz), above the 500 kHz floor in AN619 section 2. + * SI5351_OUTPUT_CLK_DIV_n equals log2(n), which is the Rx_DIV field. + * CLK6 and CLK7 do not use this helper. set_freq() chooses the smallest + * R that brings the output up to SI5351_MS67_MIN_FREQ (600 MHz / 254, + * rounded up) so an even ratio 6..254 can reach the VCO minimum. + * That is a different edge than this octave table: 36908 Hz is still + * /128 there, because /64 would leave the multisynth under 600 MHz. */ + uint64_t lower = (uint64_t)min_freq * SI5351_FREQ_MULT; + int8_t shift; + + for(shift = 7; shift >= 1; shift--) + { + uint64_t upper = lower << 1; + if((*freq >= lower) && (*freq < upper)) + { + *freq *= (1ULL << shift); + return (uint8_t)shift; + } + lower = upper; + } + + return SI5351_OUTPUT_CLK_DIV_1; +} diff --git a/tests/regression/reference/v242_f885fca/si5351.h b/tests/regression/reference/v242_f885fca/si5351.h new file mode 100644 index 0000000..b7552d0 --- /dev/null +++ b/tests/regression/reference/v242_f885fca/si5351.h @@ -0,0 +1,372 @@ +/* + * si5351.h - Si5351 library for Arduino + * + * Copyright (C) 2015 - 2019 Jason Milldrum + * Dana H. Myers + * + * Many defines derived from clk-si5351.h in the Linux kernel. + * Sebastian Hesselbarth + * Rabeeh Khoury + * + * do_div() macro derived from /include/asm-generic/div64.h in + * the Linux kernel. + * Copyright (C) 2003 Bernardo Innocenti + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SI5351_H_ +#define SI5351_H_ + +#include "Arduino.h" +#include "Wire.h" +#include + +/* Define definitions */ + +#define SI5351_BUS_BASE_ADDR 0x60 +#define SI5351_XTAL_FREQ 25000000 +#define SI5351_PLL_FIXED 80000000000ULL +#define SI5351_FREQ_MULT 100ULL +#define SI5351_DEFAULT_CLK 1000000000ULL + +#define SI5351_PLL_VCO_MIN 600000000 +#define SI5351_PLL_VCO_MAX 900000000 +#define SI5351_MULTISYNTH_MIN_FREQ 500000 +#define SI5351_MULTISYNTH_DIVBY4_FREQ 150000000 +#define SI5351_MULTISYNTH_MAX_FREQ 225000000 +#define SI5351_MULTISYNTH_SHARE_MAX 100000000 +#define SI5351_MULTISYNTH_SHARE_MIN 1024000 +#define SI5351_MULTISYNTH67_MAX_FREQ SI5351_MULTISYNTH_DIVBY4_FREQ +#define SI5351_CLKOUT_MIN_FREQ 4000 +#define SI5351_CLKOUT_MAX_FREQ SI5351_MULTISYNTH_MAX_FREQ +#define SI5351_CLKOUT67_MS_MIN SI5351_PLL_VCO_MIN / SI5351_MULTISYNTH67_A_MAX +#define SI5351_CLKOUT67_MIN_FREQ SI5351_CLKOUT67_MS_MIN / 128 +#define SI5351_CLKOUT67_MAX_FREQ SI5351_MULTISYNTH67_MAX_FREQ + +#define SI5351_PLL_A_MIN 15 +#define SI5351_PLL_A_MAX 90 +#define SI5351_PLL_B_MAX (SI5351_PLL_C_MAX-1) +#define SI5351_PLL_C_MAX 1048575 +#define SI5351_MULTISYNTH_A_MIN 6 +#define SI5351_MULTISYNTH_A_MAX 1800 +#define SI5351_MULTISYNTH67_A_MAX 254 +#define SI5351_MULTISYNTH_B_MAX (SI5351_MULTISYNTH_C_MAX-1) +#define SI5351_MULTISYNTH_C_MAX 1048575 +#define SI5351_MULTISYNTH_P1_MAX ((1<<18)-1) +#define SI5351_MULTISYNTH_P2_MAX ((1<<20)-1) +#define SI5351_MULTISYNTH_P3_MAX ((1<<20)-1) +#define SI5351_VCXO_PULL_MIN 30 +#define SI5351_VCXO_PULL_MAX 240 +#define SI5351_VCXO_MARGIN 103 + +#define SI5351_DEVICE_STATUS 0 +#define SI5351_INTERRUPT_STATUS 1 +#define SI5351_INTERRUPT_MASK 2 +#define SI5351_STATUS_SYS_INIT (1<<7) +#define SI5351_STATUS_LOL_B (1<<6) +#define SI5351_STATUS_LOL_A (1<<5) +// get_i2c_error() code for a read that returned no data. Distinct from the +// Wire endTransmission() codes (1-5). +#define SI5351_I2C_ERR_NO_DATA 0xFF +#define SI5351_STATUS_LOS (1<<3) // LOS_XTAL, matches Si5351Status.LOS +#define SI5351_STATUS_LOS_CLKIN (1<<4) +#define SI5351_STATUS_LOS_XTAL (1<<3) +#define SI5351_OUTPUT_ENABLE_CTRL 3 +#define SI5351_OEB_PIN_ENABLE_CTRL 9 +#define SI5351_PLL_INPUT_SOURCE 15 +#define SI5351_CLKIN_DIV_MASK (3<<6) +#define SI5351_CLKIN_DIV_1 (0<<6) +#define SI5351_CLKIN_DIV_2 (1<<6) +#define SI5351_CLKIN_DIV_4 (2<<6) +#define SI5351_CLKIN_DIV_8 (3<<6) +#define SI5351_PLLB_SOURCE (1<<3) +#define SI5351_PLLA_SOURCE (1<<2) + +#define SI5351_CLK0_CTRL 16 +#define SI5351_CLK1_CTRL 17 +#define SI5351_CLK2_CTRL 18 +#define SI5351_CLK3_CTRL 19 +#define SI5351_CLK4_CTRL 20 +#define SI5351_CLK5_CTRL 21 +#define SI5351_CLK6_CTRL 22 +#define SI5351_CLK7_CTRL 23 +#define SI5351_CLK_POWERDOWN (1<<7) +#define SI5351_CLK_INTEGER_MODE (1<<6) +#define SI5351_CLK_PLL_SELECT (1<<5) +#define SI5351_CLK_INVERT (1<<4) +#define SI5351_CLK_INPUT_MASK (3<<2) +#define SI5351_CLK_INPUT_XTAL (0<<2) +#define SI5351_CLK_INPUT_CLKIN (1<<2) +#define SI5351_CLK_INPUT_MULTISYNTH_0_4 (2<<2) +#define SI5351_CLK_INPUT_MULTISYNTH_N (3<<2) +#define SI5351_CLK_DRIVE_STRENGTH_MASK (3<<0) +#define SI5351_CLK_DRIVE_STRENGTH_2MA (0<<0) +#define SI5351_CLK_DRIVE_STRENGTH_4MA (1<<0) +#define SI5351_CLK_DRIVE_STRENGTH_6MA (2<<0) +#define SI5351_CLK_DRIVE_STRENGTH_8MA (3<<0) + +#define SI5351_CLK3_0_DISABLE_STATE 24 +#define SI5351_CLK7_4_DISABLE_STATE 25 +#define SI5351_CLK_DISABLE_STATE_MASK 3 +#define SI5351_CLK_DISABLE_STATE_LOW 0 +#define SI5351_CLK_DISABLE_STATE_HIGH 1 +#define SI5351_CLK_DISABLE_STATE_FLOAT 2 +#define SI5351_CLK_DISABLE_STATE_NEVER 3 + +#define SI5351_PARAMETERS_LENGTH 8 +#define SI5351_PLLA_PARAMETERS 26 +#define SI5351_PLLB_PARAMETERS 34 +#define SI5351_CLK0_PARAMETERS 42 +#define SI5351_CLK1_PARAMETERS 50 +#define SI5351_CLK2_PARAMETERS 58 +#define SI5351_CLK3_PARAMETERS 66 +#define SI5351_CLK4_PARAMETERS 74 +#define SI5351_CLK5_PARAMETERS 82 +#define SI5351_CLK6_PARAMETERS 90 +#define SI5351_CLK7_PARAMETERS 91 +#define SI5351_CLK6_7_OUTPUT_DIVIDER 92 +#define SI5351_OUTPUT_CLK_DIV_MASK (7 << 4) +#define SI5351_OUTPUT_CLK6_DIV_MASK (7 << 0) +#define SI5351_OUTPUT_CLK_DIV_SHIFT 4 +#define SI5351_OUTPUT_CLK_DIV6_SHIFT 0 +#define SI5351_OUTPUT_CLK_DIV_1 0 +#define SI5351_OUTPUT_CLK_DIV_2 1 +#define SI5351_OUTPUT_CLK_DIV_4 2 +#define SI5351_OUTPUT_CLK_DIV_8 3 +#define SI5351_OUTPUT_CLK_DIV_16 4 +#define SI5351_OUTPUT_CLK_DIV_32 5 +#define SI5351_OUTPUT_CLK_DIV_64 6 +#define SI5351_OUTPUT_CLK_DIV_128 7 +#define SI5351_OUTPUT_CLK_DIVBY4 (3<<2) + +#define SI5351_SSC_PARAM0 149 +#define SI5351_SSC_EN (1<<7) +#define SI5351_SSC_PARAM1 150 +#define SI5351_SSC_PARAM2 151 +#define SI5351_SSC_PARAM3 152 +#define SI5351_SSC_PARAM4 153 +#define SI5351_SSC_PARAM5 154 +#define SI5351_SSC_PARAM6 155 +#define SI5351_SSC_PARAM7 156 +#define SI5351_SSC_PARAM8 157 +#define SI5351_SSC_PARAM9 158 +#define SI5351_SSC_PARAM10 159 +#define SI5351_SSC_PARAM11 160 +#define SI5351_SSC_PARAM12 161 + +#define SI5351_VXCO_PARAMETERS_LOW 162 +#define SI5351_VXCO_PARAMETERS_MID 163 +#define SI5351_VXCO_PARAMETERS_HIGH 164 + +#define SI5351_CLK0_PHASE_OFFSET 165 +#define SI5351_CLK1_PHASE_OFFSET 166 +#define SI5351_CLK2_PHASE_OFFSET 167 +#define SI5351_CLK3_PHASE_OFFSET 168 +#define SI5351_CLK4_PHASE_OFFSET 169 +#define SI5351_CLK5_PHASE_OFFSET 170 + +#define SI5351_PLL_RESET 177 +#define SI5351_PLL_RESET_B (1<<7) +#define SI5351_PLL_RESET_A (1<<5) + +#define SI5351_CRYSTAL_LOAD 183 +#define SI5351_CRYSTAL_LOAD_MASK (3<<6) +/* Deprecated. AN619 Register 183, XTAL_CL[1:0]: "00: Reserved. Do not select + * this option." 01 is 6 pF, 10 is 8 pF, 11 is 10 pF (the power-up default). + * Bits 5:0 of that register are reserved and must be written as 010010b. + * This macro is still the reserved 00b code so older sketches compile. It is + * not an alias of a legal load: init() substitutes the 10 pF default and + * still writes bits 5:0 as 010010b. Prefer SI5351_CRYSTAL_LOAD_6PF, _8PF, + * or _10PF. */ +#define SI5351_CRYSTAL_LOAD_0PF (0<<6) +#define SI5351_CRYSTAL_LOAD_6PF (1<<6) +#define SI5351_CRYSTAL_LOAD_8PF (2<<6) +#define SI5351_CRYSTAL_LOAD_10PF (3<<6) + +#define SI5351_FANOUT_ENABLE 187 +#define SI5351_CLKIN_ENABLE (1<<7) +#define SI5351_XTAL_ENABLE (1<<6) +#define SI5351_MULTISYNTH_ENABLE (1<<4) + + +/* Macro definitions */ + +//#define RFRAC_DENOM ((1L << 20) - 1) +#define RFRAC_DENOM 1000000ULL + +/* + * Based on former asm-ppc/div64.h and asm-m68knommu/div64.h + * + * The semantics of do_div() are: + * + * uint32_t do_div(uint64_t *n, uint32_t base) + * { + * uint32_t remainder = *n % base; + * *n = *n / base; + * return remainder; + * } + * + * NOTE: macro parameter n is evaluated multiple times, + * beware of side effects! + */ + +# define do_div(n,base) ({ \ + uint64_t __base = (base); \ + uint64_t __rem; \ + __rem = ((uint64_t)(n)) % __base; \ + (n) = ((uint64_t)(n)) / __base; \ + __rem; \ + }) + +/* Enum definitions */ + +/* + * enum si5351_variant - SiLabs Si5351 chip variant + * @SI5351_VARIANT_A: Si5351A (8 output clocks, XTAL input) + * @SI5351_VARIANT_A3: Si5351A MSOP10 (3 output clocks, XTAL input) + * @SI5351_VARIANT_B: Si5351B (8 output clocks, XTAL/VXCO input) + * @SI5351_VARIANT_C: Si5351C (8 output clocks, XTAL/CLKIN input) + */ +/* +enum si5351_variant { + SI5351_VARIANT_A = 1, + SI5351_VARIANT_A3 = 2, + SI5351_VARIANT_B = 3, + SI5351_VARIANT_C = 4, +}; +*/ + +enum si5351_clock {SI5351_CLK0, SI5351_CLK1, SI5351_CLK2, SI5351_CLK3, + SI5351_CLK4, SI5351_CLK5, SI5351_CLK6, SI5351_CLK7}; + +enum si5351_pll {SI5351_PLLA, SI5351_PLLB}; + +enum si5351_drive {SI5351_DRIVE_2MA, SI5351_DRIVE_4MA, SI5351_DRIVE_6MA, SI5351_DRIVE_8MA}; + +enum si5351_clock_source {SI5351_CLK_SRC_XTAL, SI5351_CLK_SRC_CLKIN, SI5351_CLK_SRC_MS0, SI5351_CLK_SRC_MS}; + +enum si5351_clock_disable {SI5351_CLK_DISABLE_LOW, SI5351_CLK_DISABLE_HIGH, SI5351_CLK_DISABLE_HI_Z, SI5351_CLK_DISABLE_NEVER}; + +enum si5351_clock_fanout {SI5351_FANOUT_CLKIN, SI5351_FANOUT_XO, SI5351_FANOUT_MS}; + +enum si5351_pll_input {SI5351_PLL_INPUT_XO, SI5351_PLL_INPUT_CLKIN}; + +/* Struct definitions */ + +struct Si5351RegSet +{ + uint32_t p1; + uint32_t p2; + uint32_t p3; +}; + +struct Si5351Status +{ + uint8_t SYS_INIT; + uint8_t LOL_B; + uint8_t LOL_A; + uint8_t LOS; // Loss of signal on the crystal (LOS_XTAL, bit 3) + uint8_t REVID; + uint8_t LOS_CLKIN; // Loss of signal on CLKIN (bit 4, Si5351C only) + uint8_t LOS_XTAL; // Same as LOS +}; + +struct Si5351IntStatus +{ + uint8_t SYS_INIT_STKY; + uint8_t LOL_B_STKY; + uint8_t LOL_A_STKY; + uint8_t LOS_STKY; // Sticky LOS_XTAL (bit 3) + uint8_t LOS_CLKIN_STKY; // Sticky LOS_CLKIN (bit 4) + uint8_t LOS_XTAL_STKY; // Same as LOS_STKY +}; + +class Si5351 +{ +public: + // Select the I2C bus (TwoWire instance) and address, e.g. Si5351 si5351(&Wire1); + // A null bus pointer falls back to Wire. + Si5351(TwoWire *wire = &Wire, uint8_t i2c_addr = SI5351_BUS_BASE_ADDR); + // Address only, on the default Wire bus (kept for existing sketches). + // One overload per integer type so that any integer argument, including a + // literal 0 or NULL, selects the address path exactly as before instead of + // being ambiguous with the TwoWire * constructor. + Si5351(uint8_t i2c_addr) { setup_obj(&Wire, i2c_addr); } + Si5351(int i2c_addr) { setup_obj(&Wire, (uint8_t)i2c_addr); } + Si5351(unsigned int i2c_addr) { setup_obj(&Wire, (uint8_t)i2c_addr); } + Si5351(long i2c_addr) { setup_obj(&Wire, (uint8_t)i2c_addr); } + Si5351(unsigned long i2c_addr) { setup_obj(&Wire, (uint8_t)i2c_addr); } + Si5351(long long i2c_addr) { setup_obj(&Wire, (uint8_t)i2c_addr); } + Si5351(unsigned long long i2c_addr) { setup_obj(&Wire, (uint8_t)i2c_addr); } + bool init(uint8_t, uint32_t, int32_t); + void reset(void); + void reset_freq(enum si5351_clock); + uint8_t set_freq(uint64_t, enum si5351_clock); + uint8_t set_freq_manual(uint64_t, uint64_t, enum si5351_clock); + void set_pll(uint64_t, enum si5351_pll); + void set_ms(enum si5351_clock, struct Si5351RegSet, uint8_t, uint8_t, uint8_t); + void output_enable(enum si5351_clock, uint8_t); + void drive_strength(enum si5351_clock, enum si5351_drive); + void update_status(void); + void set_correction(int32_t, enum si5351_pll_input); + void set_phase(enum si5351_clock, uint8_t); + int32_t get_correction(enum si5351_pll_input); + void pll_reset(enum si5351_pll); + void set_ms_source(enum si5351_clock, enum si5351_pll); + void set_int(enum si5351_clock, uint8_t); + void set_clock_pwr(enum si5351_clock, uint8_t); + void set_clock_invert(enum si5351_clock, uint8_t); + void set_clock_source(enum si5351_clock, enum si5351_clock_source); + void set_clock_disable(enum si5351_clock, enum si5351_clock_disable); + void set_clock_fanout(enum si5351_clock_fanout, uint8_t); + void set_pll_input(enum si5351_pll, enum si5351_pll_input); + void set_vcxo(uint64_t, uint8_t); + void set_ref_freq(uint32_t, enum si5351_pll_input); + uint8_t si5351_write_bulk(uint8_t, uint8_t, uint8_t *); + uint8_t si5351_write(uint8_t, uint8_t); + uint8_t si5351_read(uint8_t); + uint8_t get_i2c_error(void); + struct Si5351Status dev_status = {.SYS_INIT = 0, .LOL_B = 0, .LOL_A = 0, + .LOS = 0, .REVID = 0, .LOS_CLKIN = 0, .LOS_XTAL = 0}; + struct Si5351IntStatus dev_int_status = {.SYS_INIT_STKY = 0, .LOL_B_STKY = 0, + .LOL_A_STKY = 0, .LOS_STKY = 0, .LOS_CLKIN_STKY = 0, + .LOS_XTAL_STKY = 0}; + enum si5351_pll pll_assignment[8]; + uint64_t clk_freq[8]; + uint64_t plla_freq; + uint64_t pllb_freq; + enum si5351_pll_input plla_ref_osc; + enum si5351_pll_input pllb_ref_osc; + uint32_t xtal_freq[2]; +private: + void setup_obj(TwoWire *, uint8_t); + void pll_update(enum si5351_pll, uint64_t); + uint64_t pll_calc(enum si5351_pll, uint64_t, struct Si5351RegSet *, int32_t, uint8_t); + uint64_t multisynth_calc(uint64_t, uint64_t, struct Si5351RegSet *); + uint64_t multisynth67_calc(uint64_t, uint64_t, struct Si5351RegSet *); + void update_sys_status(struct Si5351Status *); + void update_int_status(struct Si5351IntStatus *); + void ms_div(enum si5351_clock, uint8_t, uint8_t); + uint8_t select_r_div(uint64_t *, uint32_t); + int32_t ref_correction[2]; + uint8_t clkin_div; + uint8_t i2c_bus_addr; + uint8_t i2c_error; + TwoWire *_wire; + bool clk_first_set[8]; + uint8_t vcxo_pull; +}; + +#endif /* SI5351_H_ */ diff --git a/tests/regression/run_v231_checks.sh b/tests/regression/run_v231_checks.sh index a50639e..c54bb6f 100755 --- a/tests/regression/run_v231_checks.sh +++ b/tests/regression/run_v231_checks.sh @@ -166,25 +166,54 @@ if grep -q 'void reset_freq' "$LIB/src/si5351.h"; then else record reset_freq FAIL "compile error" logs/build_t_reset_freq.log; fi else record reset_freq SKIP "source has no Si5351::reset_freq()"; fi -# ---------------------------------------------------------------- 4. goldens + equivalence vs master 1ddd43e (C++ harness) +# ---------------------------------------------------------------- 3d. control registers, status bits, I2C error paths +if $CXX -std=gnu++17 -Dprivate=public -I"$CI_DIR/stubs" -I"$LIB/src" \ + "$CI_DIR/ctrl/t_ctrl.cpp" "$CI_DIR/stubs/Wire.cpp" "$LIB/src/si5351.cpp" \ + -o "$BUILD/t_ctrl" > "$LOGS/build_t_ctrl.log" 2>&1; then + if "$BUILD/t_ctrl" > "$LOGS/t_ctrl.log" 2>&1 && grep -q '^ALL PASS' "$LOGS/t_ctrl.log"; then + record ctrl_status PASS "$(grep -c '^PASS' "$LOGS/t_ctrl.log") checks, $(tail -1 "$LOGS/t_ctrl.log")" logs/t_ctrl.log + else record ctrl_status FAIL "$(grep -c '^FAIL ' "$LOGS/t_ctrl.log" || true) failing checks" logs/t_ctrl.log; fi +else record ctrl_status FAIL "compile error" logs/build_t_ctrl.log; fi + +# ---------------------------------------------------------------- 3e. planning branches (CLK6/7, set_ms, set_vcxo, set_pll clamp) +if $CXX -std=gnu++17 -Dprivate=public -I"$CI_DIR/stubs" -I"$LIB/src" \ + "$CI_DIR/plan/t_plan.cpp" "$CI_DIR/stubs/Wire.cpp" "$LIB/src/si5351.cpp" \ + -o "$BUILD/t_plan" > "$LOGS/build_t_plan.log" 2>&1; then + if "$BUILD/t_plan" > "$LOGS/t_plan.log" 2>&1 && grep -q '^ALL PASS' "$LOGS/t_plan.log"; then + record plan_branches PASS "$(grep -c '^PASS' "$LOGS/t_plan.log") checks, $(tail -1 "$LOGS/t_plan.log")" logs/t_plan.log + else record plan_branches FAIL "$(grep -c '^FAIL ' "$LOGS/t_plan.log" || true) failing checks" logs/t_plan.log; fi +else record plan_branches FAIL "compile error" logs/build_t_plan.log; fi + +# ---------------------------------------------------------------- 4. goldens + hard gate vs pinned v2.4.2 (not the old 1ddd43e INFO diff) EQ="$CI_DIR/equiv" -if $CXX $CXXFLAGS -I"$EQ" -I"$CI_DIR/stubs" -I"$LIB/src" "$EQ/harness.cpp" "$CI_DIR/stubs/Wire.cpp" "$LIB/src/si5351.cpp" -o "$BUILD/h_lib" > "$LOGS/build_harness_lib.log" 2>&1 \ - && $CXX $CXXFLAGS -I"$EQ" -I"$CI_DIR/stubs" -I"$CI_DIR/reference/master_1ddd43e" "$EQ/harness.cpp" "$CI_DIR/stubs/Wire.cpp" "$CI_DIR/reference/master_1ddd43e/si5351.cpp" -o "$BUILD/h_ref" > "$LOGS/build_harness_ref.log" 2>&1; then - "$BUILD/h_lib" "$LOGS/harness_lib.jsonl" 2> "$LOGS/run_harness_lib.log" || record run_harness_lib FAIL "harness crashed" logs/run_harness_lib.log - "$BUILD/h_ref" "$LOGS/harness_master1ddd43e.jsonl" 2> "$LOGS/run_harness_ref.log" || record run_harness_ref FAIL "harness crashed" logs/run_harness_ref.log - set +e; $PY "$EQ/compare.py" "$LOGS/harness_lib.jsonl" "$LOGS/harness_master1ddd43e.jsonl" "$LOGS/equiv_report.txt" > "$LOGS/compare.log" 2>&1; set -e - G="$(grep -E '^Goldens vs ORIGINAL C\+\+' "$LOGS/equiv_report.txt" || true)" - if [[ $G =~ ([0-9]+)\ field\ checks,\ ([0-9]+)\ mismatches ]]; then - if [[ ${BASH_REMATCH[2]} == 0 ]]; then record goldens27_vs_lib PASS "27 golden cases, ${BASH_REMATCH[1]} field checks, 0 mismatches" logs/equiv_report.txt - elif [[ $REQUIRE_FIXED == 1 ]]; then record goldens27_vs_lib INFO "${BASH_REMATCH[2]} golden mismatches (goldens lock v2.2.0/2.3.0 behaviour; fixes may change some, review the list)" logs/equiv_report.txt - else record goldens27_vs_lib FAIL "${BASH_REMATCH[2]} mismatches of ${BASH_REMATCH[1]} field checks" logs/equiv_report.txt; fi - else record goldens27_vs_lib FAIL "compare.py produced no golden summary" logs/compare.log; fi - EQL="$(grep -E '^Equivalence:' "$LOGS/equiv_report.txt" || true)" - if [[ $EQL =~ differing\ runs:\ ([0-9]+) ]]; then - record equiv_vs_master_1ddd43e INFO "${BASH_REMATCH[1]} differing runs vs master 1ddd43e (0 = behaviourally identical; fixes are expected to differ)" logs/equiv_report.txt - else record equiv_vs_master_1ddd43e FAIL "no equivalence summary" logs/compare.log; fi - NB="$(grep -E '^Bonus' "$LOGS/equiv_report.txt" || true)"; [[ -n $NB ]] && record goldens_newer_vs_lib INFO "${NB#Bonus - }" logs/equiv_report.txt -else record harness_build FAIL "compile error (see build_harness_*.log)" logs/build_harness_lib.log; fi +V242="$CI_DIR/reference/v242_f885fca" +if ( cd "$V242" && sha256sum -c SHA256SUMS ) > "$LOGS/v242_sha256.log" 2>&1; then + record v242_provenance PASS "reference/v242_f885fca matches SHA256SUMS" logs/v242_sha256.log + if $CXX $CXXFLAGS -I"$EQ" -I"$CI_DIR/stubs" -I"$LIB/src" "$EQ/harness.cpp" "$CI_DIR/stubs/Wire.cpp" "$LIB/src/si5351.cpp" -o "$BUILD/h_lib" > "$LOGS/build_harness_lib.log" 2>&1 \ + && $CXX $CXXFLAGS -I"$EQ" -I"$CI_DIR/stubs" -I"$V242" "$EQ/harness.cpp" "$CI_DIR/stubs/Wire.cpp" "$V242/si5351.cpp" -o "$BUILD/h_v242" > "$LOGS/build_harness_v242.log" 2>&1; then + "$BUILD/h_lib" "$LOGS/harness_lib.jsonl" 2> "$LOGS/run_harness_lib.log" || record run_harness_lib FAIL "harness crashed" logs/run_harness_lib.log + "$BUILD/h_v242" "$LOGS/harness_v242.jsonl" 2> "$LOGS/run_harness_v242.log" || record run_harness_v242 FAIL "harness crashed" logs/run_harness_v242.log + set +e; $PY "$EQ/compare.py" "$LOGS/harness_lib.jsonl" "$LOGS/harness_v242.jsonl" "$LOGS/equiv_report.txt" > "$LOGS/compare.log" 2>&1; set -e + G="$(grep -E '^Goldens vs ORIGINAL C\+\+' "$LOGS/equiv_report.txt" || true)" + if [[ $G =~ ([0-9]+)\ field\ checks,\ ([0-9]+)\ mismatches ]]; then + if [[ ${BASH_REMATCH[2]} == 0 ]]; then record goldens27_vs_lib PASS "27 golden cases, ${BASH_REMATCH[1]} field checks, 0 mismatches" logs/equiv_report.txt + elif [[ $REQUIRE_FIXED == 1 ]]; then record goldens27_vs_lib INFO "${BASH_REMATCH[2]} golden mismatches (goldens lock v2.2.0/2.3.0 behaviour; fixes may change some, review the list)" logs/equiv_report.txt + else record goldens27_vs_lib FAIL "${BASH_REMATCH[2]} mismatches of ${BASH_REMATCH[1]} field checks" logs/equiv_report.txt; fi + else record goldens27_vs_lib FAIL "compare.py produced no golden summary" logs/compare.log; fi + EQL="$(grep -E '^Equivalence:' "$LOGS/equiv_report.txt" || true)" + if [[ $EQL =~ differing\ runs:\ ([0-9]+) ]]; then + if [[ ${BASH_REMATCH[1]} == 0 ]]; then + record equiv_vs_v242 PASS "0 differing runs vs pinned v2.4.2 f885fca" logs/equiv_report.txt + else + record equiv_vs_v242 FAIL "${BASH_REMATCH[1]} differing runs vs pinned v2.4.2 f885fca" logs/equiv_report.txt + fi + else record equiv_vs_v242 FAIL "no equivalence summary" logs/compare.log; fi + NB="$(grep -E '^Bonus' "$LOGS/equiv_report.txt" || true)"; [[ -n $NB ]] && record goldens_newer_vs_lib INFO "${NB#Bonus - }" logs/equiv_report.txt + else record harness_build FAIL "compile error (see build_harness_*.log)" logs/build_harness_lib.log; fi +else + record v242_provenance FAIL "reference/v242_f885fca failed sha256sum -c" logs/v242_sha256.log + record equiv_vs_v242 FAIL "provenance failed; pinned reference was not built" logs/v242_sha256.log +fi # ---------------------------------------------------------------- 5. Python golden/spec suites (oracle self-consistency) for t in test_golden test_spec; do diff --git a/tests/regression/stubs/Wire.cpp b/tests/regression/stubs/Wire.cpp index 29bac60..7cda5f2 100644 --- a/tests/regression/stubs/Wire.cpp +++ b/tests/regression/stubs/Wire.cpp @@ -22,7 +22,7 @@ void Si5351Sim::reset(int pattern, bool present_) { for (int i = 0; i < 256; i++) regs[i] = pattern ? (uint8_t)((i * 37 + 11) ^ 0xA5) : 0; regs[0] &= 0x7F; // SYS_INIT clear => device ready (init() polls bit 7) - ptr = 0; dev_addr = 0x60; present = present_; + ptr = 0; dev_addr = 0x60; present = present_; empty_read = false; in_tx = false; tx_addr = 0; tx_buf.clear(); rx_buf.clear(); rx_pos = 0; events.clear(); n_writes = n_reads = 0; @@ -73,10 +73,12 @@ uint8_t TwoWire::requestFrom(uint8_t addr, uint8_t quantity) { Si5351Sim &s = g_sim; s.rx_buf.clear(); s.rx_pos = 0; bool ack = s.present && addr == s.dev_addr; - if (ack) { + if (ack && !s.empty_read) { for (uint8_t i = 0; i < quantity; i++) s.rx_buf.push_back(s.regs[(uint8_t)(s.ptr + i)]); s.ptr = (uint8_t)(s.ptr + quantity); s.n_reads++; + } else if (ack && s.empty_read) { + s.n_reads++; } char line[48]; snprintf(line, sizeof line, "R a=%02X n=%u -> ", addr, quantity); diff --git a/tests/regression/stubs/sim.h b/tests/regression/stubs/sim.h index e265075..47aeb10 100644 --- a/tests/regression/stubs/sim.h +++ b/tests/regression/stubs/sim.h @@ -9,6 +9,7 @@ struct Si5351Sim { uint8_t ptr; // device register pointer (auto-increments) uint8_t dev_addr; // address the simulated device answers on bool present; // device ACKs? + bool empty_read; // requestFrom returns no bytes after a successful write ACK // current transmission bool in_tx; uint8_t tx_addr;