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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/flash-budget.yml
Original file line number Diff line number Diff line change
@@ -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
9 changes: 8 additions & 1 deletion .github/workflows/regression.yml
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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: |
Expand Down
7 changes: 5 additions & 2 deletions tests/regression/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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.
275 changes: 275 additions & 0 deletions tests/regression/ctrl/t_ctrl.cpp
Original file line number Diff line number Diff line change
@@ -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 <stdio.h>
#include <stdint.h>
#include <string.h>
#include <string>

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;
}
24 changes: 24 additions & 0 deletions tests/regression/flash/budgets.txt
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading