diff --git a/README.md b/README.md index 841c8b5..8ad0caf 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This library is focused towards usage in RF/amateur radio applications, but it m Thanks For Your Support! ------------------------ -If you would like to support my library development efforts, I would ask that you please consider purchasing a Si5351A Breakout Board from my [online store at etherkit.com](https://www.etherkit.com) sending a [one-time PayPal tip](https://paypal.me/NT7S), or [subscribe to me on Substack](https://nt7s.substack.com) for an ongoing contribution. Thank you! +If you would like to support development of this library, please consider a [one-time PayPal tip](https://paypal.me/NT7S) or [subscribing on Substack](https://nt7s.substack.com). Source, issues, and examples are at [github.com/etherkit/Si5351Arduino](https://github.com/etherkit/Si5351Arduino). Thank you! Library Installation --------------------- @@ -26,13 +26,15 @@ Wire the SDA and SCL pins of the Si5351 to the corresponding pins on the Arduino Connect a 25 MHz or 27 MHz crystal with a load capacitance of 6, 8, or 10 pF to the Si5351 XA and XB pins. Locate the crystal as close to the Si5351 as possible and keep the traces as short as possible. Please use a SMT crystal. A crystal with leads will have too much stray capacitance. +AN619 Register 183 selects that load in XTAL_CL[1:0]: 01b is 6 pF (`SI5351_CRYSTAL_LOAD_6PF`), 10b is 8 pF (`SI5351_CRYSTAL_LOAD_8PF`), and 11b is 10 pF (`SI5351_CRYSTAL_LOAD_10PF`, the power-up default). 00b is reserved ("Do not select this option"). Bits 5:0 of register 183 are reserved and this library writes them as 010010b. `SI5351_CRYSTAL_LOAD_0PF` is deprecated: it is that reserved 00b code, kept so older sketches still compile, and `init()` substitutes the 10 pF default instead of writing it. + Changes from v1 to v2 --------------------- The public interface to the v2 library is similar to the v1 library, but a few of the most-used methods have had their signatures changed, so your old programs won't compile right out-of-the-box after a library upgrade. Most importantly, the _init()_ and _set_freq()_ methods are different, so you'll at least need to change these calls in your old sketches. The _init()_ method now has three parameters: the crystal load capacitance, the reference frequency, and the frequency correction value (with this last parameter being a new addition). You'll need to add that third parameter to your old _init()_ calls, but then you can delete any _set_correction()_ calls after that (unless you explicitly are changing the frequency correction after the initialization). -The _set_freq()_ method is now more streamlined and only requires two parameters: the desired output frequency (from 4 kHz to 225 MHz) and clock output. In your old code, you can delete the 2nd parameter in _set_freq()_, which was the PLL frequency. In case you want to do things manually, there is now a new method called _set_freq_manual()_ (see below for details). +The _set_freq()_ method is now more streamlined and only requires two parameters: the desired output frequency (from 4 kHz to 200 MHz on CLK0 through CLK5) and clock output. In your old code, you can delete the 2nd parameter in _set_freq()_, which was the PLL frequency. In case you want to do things manually, there is now a new method called _set_freq_manual()_ (see below for details). Those two changes should cover nearly all upgrade scenarios, unless you were doing some lower-level use of the Si5351. @@ -83,7 +85,7 @@ The nominal status for each of those flags is a 0. When the program indicates 1, A Brief Word about the Si5351 Architecture ------------------------------------------ -The Si5351 consists of two main stages: two PLLs which are locked to the reference oscillator (a 25/27 MHz crystal) and which can be set from 600 to 900 MHz, and the output (multisynth) clocks which are locked to a PLL of choice and can be set from 500 kHz to 200 MHz (per the datasheet, although it does seem to be possible to set an output up to 225 MHz). +The Si5351 consists of two main stages: two PLLs which are locked to the reference oscillator (a 25/27 MHz crystal) and which can be set from 600 to 900 MHz, and the output (multisynth) clocks which are locked to a PLL of choice and can be set from 500 kHz to 200 MHz. As of v2.4.1, `set_freq()` and `set_freq_manual()` clamp CLK0 through CLK5 to 200 MHz. See _Frequency Limits_ below. The compatibility macros `SI5351_CLKOUT_MAX_FREQ` and `SI5351_MULTISYNTH_MAX_FREQ` still contain 225000000. The B variant has an additional VCXO stage with control voltage pin which can be used as a reference synth for a clock output (PLLB must be used as the source for any VCXO output clock). @@ -109,10 +111,12 @@ Instead of letting the library choose a PLL frequency for your chosen output fre When you are setting the PLL manually you need to be mindful of the limits of the IC. The multisynth (MS0 through MS5) is a fractional PLL, with limits described in AN619 as: ->Valid Multisynth divider ratios are 4, 6, 8, and any fractional value between 8 + 1/1,048,575 and 900 + 0/1. -This means that if any output is greater than 112.5 MHz (900 MHz/8), then this output frequency sets one +>Valid Multisynth divider ratios are 4, 6, 8, and any fractional value between 8 + 1/1,048,575 and 2048. This +means that if any output is greater than 112.5 MHz (900 MHz/8), then this output frequency sets one of the VCO frequencies. +That quote is AN619 rev. 0.8, section 2.1.1. In this library, `set_freq_manual()` accepts ratio 4 (divide-by-4, output at least 150 MHz), exactly 6, or at least 8, and returns 1 without writing otherwise. `SI5351_MULTISYNTH_A_MAX` is 1800. + To put this in other words, if you want to manually set the PLL and wish to have an output frequency greater than 100 MHz (changed in this library from the stated 112.5 MHz due to stability issues which were noticed), then the choice of PLL frequency is dictated by the choice of output frequency, and will need to be an even multiple of 4, 6, or 8. Further Details @@ -286,10 +290,49 @@ Default drive strength is 2 mA on each output. Constraints ----------- * Two multisynths cannot share a PLL with when both outputs are >= 100 MHz. The library will refuse to set another multisynth to a frequency in that range if another multisynth sharing the same PLL is already within that frequency range. -* Setting phase will be limited in the extreme edges of the output tuning ranges. Because the phase register is 7-bits in size and is denominated in units representing 1/4 the PLL period, not all phases can be set for all output frequencies. For example, if you need a 90° phase shift, the lowest frequency you can set it at is 4.6875 MHz (600 MHz PLL/128). +* Setting phase will be limited in the extreme edges of the output tuning ranges. The phase register is PHOFF[6:0] (AN619 Register 165; bit 7 is reserved), and one count is 1/4 of the PLL period. `set_phase()` keeps only those 7 bits, so the largest word is 127. A 90° output shift needs a phase word equal to the PLL/output ratio. At the lowest PLL frequency, 600 MHz, the lowest such output is 600 MHz / 127 (4.724409 MHz). The divider must be greater than 8, and the multisynth integer-mode bit must be clear (AN619 section 6). * The frequency range of Multisynth 6 and 7 is ~18.45 kHz to 150 MHz. The library assigns PLLB to these two multisynths, so if you choose to use both, then both frequencies must be an even divisor of the PLL frequency (between 6 and 254), so plan accordingly. You can see the current PLLB frequency by accessing the _pllb_freq_ public member. * VCXO pull range can be ±30 to ±240 ppm +Frequency Limits +---------------- +Numbers below are what v2.4.1 programs, checked against `src/si5351.cpp`, `src/si5351.h`, AN619 rev. 0.8, and the Si5351A/B/C-B datasheet rev. 1.3. Frequencies passed to `set_freq()` are in units of 0.01 Hz. + +### Multisynth ratios + +AN619 section 2.1.1 and section 4.1.2: MS0 through MS5 may use 4, 6, 8, or any fractional ratio from 8 + 1/1,048,575 through 2048. Section 4.1.3: for 150 MHz < Fout ≤ 200 MHz the ratio is 4 (divide-by-4) and the VCO is 4 times the output. Section 2.2: MS6 and MS7 are even integers from 6 through 254 only. The PLL feedback ratio is 15 through 90, and the VCO is 600 to 900 MHz (sections 2 and 3.2). + +`set_freq_manual()` returns 1 and writes nothing unless the CLK0–CLK5 ratio is 4 (divide-by-4, output at least 150 MHz), exactly 6, or at least 8. `SI5351_MULTISYNTH_A_MIN` is 6 and `SI5351_MULTISYNTH_A_MAX` is 1800 (900 MHz / 500 kHz), which is below the AN619 maximum of 2048. `SI5351_MULTISYNTH67_A_MAX` is 254. `SI5351_PLL_A_MIN` is 15, `SI5351_PLL_A_MAX` is 90, `SI5351_PLL_VCO_MIN` is 600 MHz, and `SI5351_PLL_VCO_MAX` is 900 MHz. + +### Minimum output frequency + +The datasheet (rev. 1.3) specifies 2.5 kHz to 200 MHz. AN619 section 2 also says the R divider reaches 2.5 kHz; section 4.2.2 still says 8 kHz. The output multisynth itself is specified from 500 kHz (AN619 section 2), and the largest R divider is 128 (AN619 Register 44, R0_DIV = 111b), so 500 kHz / 128 = 3.90625 kHz. + +`SI5351_CLKOUT_MIN_FREQ` is 4000. On CLK0–CLK5, `set_freq()` and `set_freq_manual()` raise a non-zero request below 4 kHz to 4 kHz. 4 kHz times 128 is 512 kHz, which is above the 500 kHz multisynth floor. `select_r_div()` then uses one R divider per octave (low edge inclusive). Divide-by-64 is only 8 kHz up to 16 kHz; the next octave, 16 kHz up to 32 kHz, is divide-by-32. + +| CLK0–CLK5 output | R divider | Rx_DIV | Multisynth frequency | +| --- | --- | --- | --- | +| 4 kHz up to 8 kHz | 128 | 111b | 512 kHz up to 1.024 MHz | +| 8 kHz up to 16 kHz | 64 | 110b | 512 kHz up to 1.024 MHz | +| 16 kHz up to 32 kHz | 32 | 101b | 512 kHz up to 1.024 MHz | +| 32 kHz up to 64 kHz | 16 | 100b | 512 kHz up to 1.024 MHz | +| 64 kHz up to 128 kHz | 8 | 011b | 512 kHz up to 1.024 MHz | +| 128 kHz up to 256 kHz | 4 | 010b | 512 kHz up to 1.024 MHz | +| 256 kHz up to 512 kHz | 2 | 001b | 512 kHz up to 1.024 MHz | +| 512 kHz and above | 1 | 000b | the output frequency | + +CLK6 and CLK7 are even-integer dividers, so their floor is higher. `SI5351_CLKOUT67_MIN_FREQ` is 18454 Hz, from 600 MHz / 254 / 128 using integer division. `set_freq()` clamps a request below `SI5351_CLKOUT67_MIN` up to 18454.73 Hz. It then picks the smallest R divider that brings the multisynth up to at least 600 MHz / 254, so the ratio stays inside 6..254 and the VCO stays at or above 600 MHz. That choice is not the CLK0–CLK5 octave table (36908 Hz is still divide-by-128). `set_freq()` also clamps CLK6 and CLK7 to just under 150 MHz, because MS6 and MS7 have no divide-by-4 mode. `set_freq_manual()` does not apply that clamp. + +### Minimum quadrature frequency + +AN619 Register 165 defines CLK0_PHOFF[6:0]. Bit 7 is reserved ("Only write 0 to this bit"). One count is Tvco/4. A 90° shift of the output is one quarter of the output period, which is Fvco/Fout counts. The 14.1 MHz example above uses 705 MHz / 14.1 MHz = 50. `set_phase()` masks the word with 0b01111111, so 127 is the largest value it will write. Phase offset applies only when the output divider is greater than 8 and MSx_INT is 0 (AN619 section 6). + +The lowest VCO is 600 MHz (`SI5351_PLL_VCO_MIN`). The lowest 90° output on that VCO is 600 MHz / 127, which is 4.724409 MHz. Integer division of those two constants is 4,724,409 Hz. + +### 200 MHz cap on CLK0–CLK5 (v2.4.1) + +The datasheet maximum is 200 MHz. AN619 section 4.1.3 covers 150 MHz < Fout ≤ 200 MHz on multisynths 0–5 with divide-by-4. v2.4.1 defines `SI5351_FOUT_MAX` as 200000000 and `set_freq()` / `set_freq_manual()` clamp a CLK0–CLK5 request above 200 MHz to 200 MHz. `SI5351_CLKOUT_MAX_FREQ` and `SI5351_MULTISYNTH_MAX_FREQ` are still 225000000; those macros are what older code compared against, and they are not the frequency `set_freq()` programs. `set_freq()` clamps CLK6 and CLK7 to 149,999,999.99 Hz. `set_freq_manual()` does not: `set_freq_manual(150 MHz, 900 MHz PLL, CLK6)` is ratio 6 and returns 0. + Public Methods -------------- ### init() @@ -759,6 +802,10 @@ Changelog * Unreleased + * Lock the CLK0–CLK5 R-divider octave table in `select_r_div()` with a regression test. Divide-by-64 is only the [2F, 4F) band; the next band is divide-by-32. CLK6/CLK7 edges are what `set_freq()` writes to register 92 (#89) + * Document `SI5351_CRYSTAL_LOAD_0PF`: AN619 Register 183 says XTAL_CL 00b is reserved, and `init()` keeps the 10 pF default + * Document multisynth ratio limits, the 4 kHz CLK0–CLK5 floor, the 600 MHz/127 quadrature floor, and the v2.4.1 200 MHz CLK0–CLK5 cap + * Replace the dead etherkit.com links with this GitHub repository (#102) * Add `reset_freq()` to set one CLK output back to 0 Hz, disable it, and clear its first-set flag so the next `set_freq()` enables it again, the same as the first call. Pass a valid `si5351_clock` value (`SI5351_CLK0`..`SI5351_CLK7`); out-of-range values are not supported. Other outputs are left unchanged. Thanks to KT315AM (#104) * v2.4.1 @@ -882,4 +929,4 @@ Changelog [1]: http://www.silabs.com - [2]: https://www.etherkit.com + [2]: https://github.com/etherkit/Si5351Arduino diff --git a/src/si5351.cpp b/src/si5351.cpp index c5a4f03..f1fbb42 100644 --- a/src/si5351.cpp +++ b/src/si5351.cpp @@ -185,9 +185,10 @@ bool Si5351::init(uint8_t xtal_load_c, uint32_t xo_freq, int32_t corr) status_reg = si5351_read(SI5351_DEVICE_STATUS); } while (status_reg >> 7 == 1); - // Set crystal load capacitance. XTAL_CL = 00b (SI5351_CRYSTAL_LOAD_0PF) - // is reserved ("do not select", AN619 reg 183) and 0 pF is not - // supported, so keep the chip default of 10 pF instead. + // 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) { @@ -1896,10 +1897,21 @@ void Si5351::ms_div(enum si5351_clock clk, uint8_t r_div, uint8_t div_by_4) uint8_t Si5351::select_r_div(uint64_t *freq, uint32_t min_freq) { - /* Unified R-divider selection for CLK0-5 (min_freq = CLKOUT_MIN_FREQ) - * and CLK6-7 (min_freq = CLKOUT67_MIN_FREQ). Same band thresholds as - * the former select_r_div / select_r_div_ms67 pair. - * SI5351_OUTPUT_CLK_DIV_n enum values equal log2(n). */ + /* 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; diff --git a/src/si5351.h b/src/si5351.h index f94c0c3..b7552d0 100644 --- a/src/si5351.h +++ b/src/si5351.h @@ -183,7 +183,14 @@ #define SI5351_CRYSTAL_LOAD 183 #define SI5351_CRYSTAL_LOAD_MASK (3<<6) -#define SI5351_CRYSTAL_LOAD_0PF (0<<6) /* reserved in AN619, unsupported: init() keeps 10 pF */ +/* 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) diff --git a/tests/regression/README.md b/tests/regression/README.md index fbead5a..0b41477 100644 --- a/tests/regression/README.md +++ b/tests/regression/README.md @@ -31,6 +31,7 @@ Exit code: 0 = every check PASS/XFAIL/SKIP/INFO; 1 = at least one unexpected FAI | `acceptance_A01..A25` | `v231/test_v231.py` AN619/datasheet oracle. **Default mode: a failing test is XFAIL** (master is expected to fail all 25). `--require-fixed`: a failing test is FAIL | per mode | | `must_not_break` | 776 register/state/return/reg177 locks from `v231/must_not_break.json` | yes | | `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 | | `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 | diff --git a/tests/regression/rdiv/t_rdiv.cpp b/tests/regression/rdiv/t_rdiv.cpp new file mode 100644 index 0000000..d39338e --- /dev/null +++ b/tests/regression/rdiv/t_rdiv.cpp @@ -0,0 +1,394 @@ +/* R-divider edges (issue #89), AN619 Register 183 XTAL_CL, and a check that + * README limit numbers match the header (and SI5351_FOUT_MAX in si5351.cpp). + * + * CLK0-5: select_r_div() octave table, plus the register 44 field set_freq() + * writes. CLK6/7: set_freq() itself. That path does not call select_r_div(); + * it picks the smallest R whose doubled frequency reaches 600 MHz/254. + * + * Compile with -Dprivate=public so select_r_div() can be called. + * Usage: t_rdiv README.md src/si5351.cpp + */ +#include "si5351.h" +#include "sim.h" +#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) + +/* MS6/7 minimum in 0.01 Hz units: ceil(600 MHz / 254). Same expression as + * SI5351_MS67_MIN_FREQ in si5351.cpp. */ +static uint64_t ms67_min_centi(void) +{ + return (SI5351_PLL_VCO_MIN * (uint64_t)SI5351_FREQ_MULT + + SI5351_MULTISYNTH67_A_MAX - 1) / SI5351_MULTISYNTH67_A_MAX; +} + +/* What set_freq() does for a solo CLK6/7: clamp up to SI5351_CLKOUT67_MIN, + * then the smallest R in 0..7 with fout << R >= MS67_MIN. */ +static uint8_t r_for_clk67(uint64_t centi) +{ + uint64_t ms = ms67_min_centi(); + uint64_t floor = (ms + 127) / 128; + uint8_t r = 0; + + if (centi < floor) + { + centi = floor; + } + while (centi < ms && r < 7) + { + centi <<= 1; + r++; + } + return r; +} + +/* One octave per R code for CLK0-5. Low edge inclusive, high edge exclusive. + * The distinct-code mask is built from the divider select_r_div() returned, + * not from the loop index, so a duplicated band leaves a hole. */ +static void check_octaves(Si5351 &si, uint32_t min_hz, const char *tag) +{ + uint32_t seen = 0; + int shift; + + for (shift = 7; shift >= 1; shift--) + { + uint64_t lo_hz = (uint64_t)min_hz << (7 - shift); + uint64_t hi_hz = lo_hz << 1; + uint32_t mul = 1u << shift; + uint64_t f; + uint8_t r; + char buf[96]; + + f = lo_hz * SI5351_FREQ_MULT; + r = si.select_r_div(&f, min_hz); + snprintf(buf, sizeof buf, "%s /%u low %llu Hz -> code %u", tag, mul, + (unsigned long long)lo_hz, r); + CHECK(r == (uint8_t)shift && f == lo_hz * SI5351_FREQ_MULT * mul, buf); + seen |= (1u << r); + if (min_hz == SI5351_CLKOUT_MIN_FREQ) + { + CHECK(f >= 500000ULL * SI5351_FREQ_MULT, "multisynth at/above 500 kHz"); + } + + f = hi_hz * SI5351_FREQ_MULT - 1; + r = si.select_r_div(&f, min_hz); + snprintf(buf, sizeof buf, "%s /%u just below %llu Hz -> code %u", tag, mul, + (unsigned long long)hi_hz, r); + CHECK(r == (uint8_t)shift && f == (hi_hz * SI5351_FREQ_MULT - 1) * mul, buf); + + f = hi_hz * SI5351_FREQ_MULT; + r = si.select_r_div(&f, min_hz); + snprintf(buf, sizeof buf, "%s boundary %llu Hz is not /%u (got code %u)", tag, + (unsigned long long)hi_hz, mul, r); + CHECK(r != (uint8_t)shift, buf); + } + + { + uint64_t start_hz = (uint64_t)min_hz << 7; + uint64_t f = start_hz * SI5351_FREQ_MULT; + uint8_t r = si.select_r_div(&f, min_hz); + char buf[96]; + snprintf(buf, sizeof buf, "%s /1 from %llu Hz -> code %u", tag, + (unsigned long long)start_hz, r); + CHECK(r == SI5351_OUTPUT_CLK_DIV_1 && f == start_hz * SI5351_FREQ_MULT, buf); + seen |= (1u << r); + } + + CHECK(seen == 0xFFu, "eight distinct R codes from select_r_div (1,2,4,8,16,32,64,128)"); +} + +static void check_xtal(uint8_t load, uint8_t expect, const char *msg) +{ + g_sim.reset(0, true); + Si5351 si; + bool ok = si.init(load, 0, 0); + CHECK(ok, "init found the device"); + CHECK(g_sim.regs[SI5351_CRYSTAL_LOAD] == expect, msg); + CHECK((g_sim.regs[SI5351_CRYSTAL_LOAD] >> 6) != 0, "XTAL_CL is not reserved 00b"); + CHECK((g_sim.regs[SI5351_CRYSTAL_LOAD] & 0x3F) == 0x12, "reg 183 bits 5:0 are 010010b"); +} + +static uint8_t reg92_r(enum si5351_clock clk) +{ + uint8_t v = g_sim.regs[SI5351_CLK6_7_OUTPUT_DIVIDER]; + if (clk == SI5351_CLK6) + { + return v & 7; + } + return (v >> SI5351_OUTPUT_CLK_DIV_SHIFT) & 7; +} + +/* Register 92 after a real set_freq(). CLK6 is bits 2:0, CLK7 is bits 6:4. */ +static void check_clk67(uint64_t centi, uint8_t expect, const char *name, uint32_t *seen) +{ + enum si5351_clock clks[2] = { SI5351_CLK6, SI5351_CLK7 }; + int i; + + CHECK(r_for_clk67(centi) == expect, "edge table matches 600 MHz/254 formula"); + for (i = 0; i < 2; i++) + { + enum si5351_clock clk = clks[i]; + Si5351 si; + uint8_t got; + char buf[128]; + + g_sim.reset(0, true); + CHECK(si.init(SI5351_CRYSTAL_LOAD_8PF, 0, 0), "init before CLK6/7 set_freq"); + snprintf(buf, sizeof buf, "%s CLK%d set_freq", name, (int)clk); + CHECK(si.set_freq(centi, clk) == 0, buf); + got = reg92_r(clk); + snprintf(buf, sizeof buf, "%s CLK%d reg92 R code %u (got %u)", name, (int)clk, expect, got); + CHECK(got == expect, buf); + if (clk == SI5351_CLK6 && seen) + { + *seen |= (1u << got); + } + } +} + +static char *read_file(const char *path) +{ + FILE *fp = fopen(path, "rb"); + long n; + char *b; + + if (!fp) + { + return NULL; + } + if (fseek(fp, 0, SEEK_END) != 0 || (n = ftell(fp)) < 0) + { + fclose(fp); + return NULL; + } + rewind(fp); + b = (char *)malloc((size_t)n + 1); + if (!b || fread(b, 1, (size_t)n, fp) != (size_t)n) + { + free(b); + fclose(fp); + return NULL; + } + b[n] = 0; + fclose(fp); + return b; +} + +static unsigned long long parse_define(const char *src, const char *name) +{ + char key[80]; + const char *p; + snprintf(key, sizeof key, "#define %s", name); + p = strstr(src, key); + if (!p) + { + return 0; + } + p += strlen(key); + while (*p == ' ' || *p == '\t') + { + p++; + } + return strtoull(p, NULL, 0); +} + +static void require_text(const char *readme, const char *needle) +{ + char buf[160]; + snprintf(buf, sizeof buf, "README contains: %s", needle); + CHECK(strstr(readme, needle) != NULL, buf); +} + +static void check_readme(const char *readme_path, const char *cpp_path) +{ + char *readme = read_file(readme_path); + char *cpp = read_file(cpp_path); + char buf[160]; + unsigned long long fout_max; + unsigned long q; + uint64_t floor_c, cap_c, cap_hz; + + CHECK(readme != NULL, "README opened"); + CHECK(cpp != NULL, "si5351.cpp opened"); + if (!readme || !cpp) + { + free(readme); + free(cpp); + return; + } + + fout_max = parse_define(cpp, "SI5351_FOUT_MAX"); + snprintf(buf, sizeof buf, "parsed SI5351_FOUT_MAX (%llu)", fout_max); + CHECK(fout_max == 200000000ULL, buf); + snprintf(buf, sizeof buf, "`SI5351_FOUT_MAX` as %llu", fout_max); + require_text(readme, buf); + + snprintf(buf, sizeof buf, "`SI5351_CLKOUT_MIN_FREQ` is %lu", + (unsigned long)SI5351_CLKOUT_MIN_FREQ); + require_text(readme, buf); + snprintf(buf, sizeof buf, "`SI5351_CLKOUT67_MIN_FREQ` is %lu Hz", + (unsigned long)SI5351_CLKOUT67_MIN_FREQ); + require_text(readme, buf); + snprintf(buf, sizeof buf, "`SI5351_MULTISYNTH_A_MIN` is %lu", + (unsigned long)SI5351_MULTISYNTH_A_MIN); + require_text(readme, buf); + snprintf(buf, sizeof buf, "`SI5351_MULTISYNTH_A_MAX` is %lu", + (unsigned long)SI5351_MULTISYNTH_A_MAX); + require_text(readme, buf); + snprintf(buf, sizeof buf, "`SI5351_MULTISYNTH67_A_MAX` is %lu", + (unsigned long)SI5351_MULTISYNTH67_A_MAX); + require_text(readme, buf); + snprintf(buf, sizeof buf, "`SI5351_PLL_A_MIN` is %lu", + (unsigned long)SI5351_PLL_A_MIN); + require_text(readme, buf); + snprintf(buf, sizeof buf, "`SI5351_PLL_A_MAX` is %lu", + (unsigned long)SI5351_PLL_A_MAX); + require_text(readme, buf); + snprintf(buf, sizeof buf, "still contain %lu", + (unsigned long)SI5351_CLKOUT_MAX_FREQ); + require_text(readme, buf); + + floor_c = (ms67_min_centi() + 127) / 128; + snprintf(buf, sizeof buf, "%llu.%02llu Hz", + (unsigned long long)(floor_c / 100), + (unsigned long long)(floor_c % 100)); + require_text(readme, buf); + + cap_c = (uint64_t)SI5351_MULTISYNTH_DIVBY4_FREQ * SI5351_FREQ_MULT - 1; + cap_hz = cap_c / 100; + snprintf(buf, sizeof buf, "%llu,%03llu,%03llu.%02llu", + (unsigned long long)(cap_hz / 1000000ULL), + (unsigned long long)((cap_hz / 1000ULL) % 1000ULL), + (unsigned long long)(cap_hz % 1000ULL), + (unsigned long long)(cap_c % 100)); + require_text(readme, buf); + require_text(readme, "`set_freq()` clamps CLK6 and CLK7 to 149,999,999.99 Hz"); + require_text(readme, "set_freq_manual(150 MHz, 900 MHz PLL, CLK6)"); + + q = SI5351_PLL_VCO_MIN / 127; + snprintf(buf, sizeof buf, "%lu,%03lu,%03lu Hz", + q / 1000000UL, (q / 1000UL) % 1000UL, q % 1000UL); + require_text(readme, buf); + + require_text(readme, "from 4 kHz to 200 MHz"); + CHECK(strstr(readme, "from 4 kHz to 225 MHz") == NULL, + "README does not say set_freq runs to 225 MHz"); + + free(readme); + free(cpp); +} + +int main(int argc, char **argv) +{ + Si5351 si; + uint32_t seen67 = 0; + /* Literal edges, in 0.01 Hz units. 36908 Hz is the case that the + * CLKOUT67_MIN_FREQ octave table gets wrong (/64); set_freq writes /128 + * because 36908*64 Hz is still under 600 MHz / 254. */ + static const struct { uint64_t centi; uint8_t r; const char *name; int band_low; } edges[] = { + { 1845472ULL, 7, "below 18454.73 Hz clamps into /128", 0 }, + { 1845473ULL, 7, "/128 from 18454.73 Hz", 1 }, + { 3690800ULL, 7, "36908 Hz is /128", 0 }, + { 3690944ULL, 7, "just below /64", 0 }, + { 3690945ULL, 6, "/64 from 36909.45 Hz", 1 }, + { 7381889ULL, 6, "just below /32", 0 }, + { 7381890ULL, 5, "/32 from 73818.90 Hz", 1 }, + { 14763779ULL, 5, "just below /16", 0 }, + { 14763780ULL, 4, "/16 from 147637.80 Hz", 1 }, + { 29527559ULL, 4, "just below /8", 0 }, + { 29527560ULL, 3, "/8 from 295275.60 Hz", 1 }, + { 59055118ULL, 3, "just below /4", 0 }, + { 59055119ULL, 2, "/4 from 590551.19 Hz", 1 }, + { 118110236ULL, 2, "just below /2", 0 }, + { 118110237ULL, 1, "/2 from 1181102.37 Hz", 1 }, + { 236220472ULL, 1, "just below /1", 0 }, + { 236220473ULL, 0, "/1 from 2362204.73 Hz", 1 }, + }; + size_t i; + + if (argc != 3) + { + fprintf(stderr, "usage: %s README.md src/si5351.cpp\n", argv[0]); + return 2; + } + + check_octaves(si, SI5351_CLKOUT_MIN_FREQ, "CLK0-5"); + + { + uint64_t f = (uint64_t)(SI5351_CLKOUT_MIN_FREQ - 1) * SI5351_FREQ_MULT; + uint64_t orig = f; + uint8_t r = si.select_r_div(&f, SI5351_CLKOUT_MIN_FREQ); + CHECK(r == SI5351_OUTPUT_CLK_DIV_1 && f == orig, "below 4 kHz: select_r_div does not scale"); + } + + for (i = 0; i < sizeof edges / sizeof edges[0]; i++) + { + check_clk67(edges[i].centi, edges[i].r, edges[i].name, + edges[i].band_low ? &seen67 : NULL); + } + CHECK(seen67 == 0xFFu, "eight distinct CLK6 reg92 R codes from set_freq"); + + check_xtal(SI5351_CRYSTAL_LOAD_6PF, 0x52, "reg183 6 pF is 0x52"); + check_xtal(SI5351_CRYSTAL_LOAD_8PF, 0x92, "reg183 8 pF is 0x92"); + check_xtal(SI5351_CRYSTAL_LOAD_10PF, 0xD2, "reg183 10 pF is 0xD2"); + check_xtal(SI5351_CRYSTAL_LOAD_0PF, 0xD2, "reg183 reserved 0 pF keeps 10 pF (0xD2)"); + CHECK(SI5351_CRYSTAL_LOAD_0PF == (0 << 6), "0 pF macro is still the reserved code"); + + g_sim.reset(0, true); + CHECK(si.init(SI5351_CRYSTAL_LOAD_8PF, 0, 0), "init before set_freq"); + CHECK(si.set_freq(8000ULL * SI5351_FREQ_MULT, SI5351_CLK0) == 0, "set_freq 8 kHz"); + CHECK(((g_sim.regs[SI5351_CLK0_PARAMETERS + 2] >> 4) & 7) == SI5351_OUTPUT_CLK_DIV_64, + "reg44 R = /64 at 8 kHz"); + CHECK(si.set_freq(16000ULL * SI5351_FREQ_MULT, SI5351_CLK0) == 0, "set_freq 16 kHz"); + CHECK(((g_sim.regs[SI5351_CLK0_PARAMETERS + 2] >> 4) & 7) == SI5351_OUTPUT_CLK_DIV_32, + "reg44 R = /32 at 16 kHz"); + CHECK(si.set_freq(4000ULL * SI5351_FREQ_MULT, SI5351_CLK0) == 0, "set_freq 4 kHz"); + CHECK(((g_sim.regs[SI5351_CLK0_PARAMETERS + 2] >> 4) & 7) == SI5351_OUTPUT_CLK_DIV_128, + "reg44 R = /128 at 4 kHz"); + CHECK(si.set_freq(1000ULL * SI5351_FREQ_MULT, SI5351_CLK0) == 0, "set_freq 1 kHz clamps"); + CHECK(si.clk_freq[0] == 4000ULL * SI5351_FREQ_MULT, "1 kHz request stored as 4 kHz"); + + CHECK(si.set_freq(225000000ULL * SI5351_FREQ_MULT, SI5351_CLK0) == 0, "set_freq 225 MHz"); + CHECK(si.clk_freq[0] == 200000000ULL * SI5351_FREQ_MULT, "225 MHz stored as 200 MHz"); + CHECK(SI5351_CLKOUT_MAX_FREQ == 225000000UL, "compatibility macro still says 225 MHz"); + + /* set_freq stops CLK6 just under 150 MHz. set_freq_manual(150 MHz, 900 MHz) + * is an even ratio of 6 and is accepted. */ + { + uint64_t f150 = 150000000ULL * SI5351_FREQ_MULT; + uint64_t pll900 = 900000000ULL * SI5351_FREQ_MULT; + Si5351 s2; + + g_sim.reset(0, true); + CHECK(s2.init(SI5351_CRYSTAL_LOAD_8PF, 0, 0), "init before CLK6 150 MHz"); + CHECK(s2.set_freq(f150, SI5351_CLK6) == 0, "set_freq 150 MHz CLK6"); + CHECK(s2.clk_freq[SI5351_CLK6] == f150 - 1, "set_freq stores 149999999.99 Hz"); + CHECK(reg92_r(SI5351_CLK6) == 0, "set_freq 150 MHz CLK6 uses R=/1"); + + g_sim.reset(0, true); + CHECK(s2.init(SI5351_CRYSTAL_LOAD_8PF, 0, 0), "init before CLK6 manual 150 MHz"); + CHECK(s2.set_freq_manual(f150, pll900, SI5351_CLK6) == 0, + "set_freq_manual 150 MHz / 900 MHz CLK6 returns 0"); + CHECK(s2.clk_freq[SI5351_CLK6] == f150, "manual 150 MHz is stored as 150 MHz"); + CHECK(reg92_r(SI5351_CLK6) == 0, "manual 150 MHz CLK6 uses R=/1"); + CHECK(g_sim.regs[SI5351_CLK6_PARAMETERS] == 6, "manual 150/900 writes MS6 ratio 6"); + } + + si.set_phase(SI5351_CLK0, 255); + CHECK(g_sim.regs[SI5351_CLK0_PHASE_OFFSET] == 127, "set_phase masks to 127"); + si.set_phase(SI5351_CLK0, 128); + CHECK(g_sim.regs[SI5351_CLK0_PHASE_OFFSET] == 0, "phase word 128 does not fit in bits 6:0"); + CHECK(SI5351_PLL_VCO_MIN / 127 == 4724409UL, "600 MHz / 127 = 4724409 Hz"); + + check_readme(argv[1], argv[2]); + + printf("%s (%d failures)\n", fails ? "FAILED" : "ALL PASS", fails); + return fails ? 1 : 0; +} diff --git a/tests/regression/run_v231_checks.sh b/tests/regression/run_v231_checks.sh index b6b53cb..a50639e 100755 --- a/tests/regression/run_v231_checks.sh +++ b/tests/regression/run_v231_checks.sh @@ -146,7 +146,16 @@ if [[ $HAS_TWOWIRE == 1 ]]; then else record t106_twowire FAIL "compile error" logs/build_t106.log; fi else record t106_twowire SKIP "source has no TwoWire* constructor (pre-#106)"; fi -# ---------------------------------------------------------------- 3b. reset_freq() (SKIP when the pinned source has no such method) +# ---------------------------------------------------------------- 3b. R-divider bands (issue #89) and XTAL_CL (AN619 Register 183) +if $CXX -std=gnu++17 -Dprivate=public -I"$CI_DIR/stubs" -I"$LIB/src" \ + "$CI_DIR/rdiv/t_rdiv.cpp" "$CI_DIR/stubs/Wire.cpp" "$LIB/src/si5351.cpp" \ + -o "$BUILD/t_rdiv" > "$LOGS/build_t_rdiv.log" 2>&1; then + if "$BUILD/t_rdiv" "$LIB/README.md" "$LIB/src/si5351.cpp" > "$LOGS/t_rdiv.log" 2>&1 && grep -q '^ALL PASS' "$LOGS/t_rdiv.log"; then + record rdiv_xtal PASS "$(grep -c '^PASS' "$LOGS/t_rdiv.log") checks, $(tail -1 "$LOGS/t_rdiv.log")" logs/t_rdiv.log + else record rdiv_xtal FAIL "$(grep -c '^FAIL' "$LOGS/t_rdiv.log" || true) failing checks" logs/t_rdiv.log; fi +else record rdiv_xtal FAIL "compile error" logs/build_t_rdiv.log; fi + +# ---------------------------------------------------------------- 3c. reset_freq() (SKIP when the pinned source has no such method) if grep -q 'void reset_freq' "$LIB/src/si5351.h"; then if $CXX -std=gnu++17 -Dprivate=public -I"$CI_DIR/stubs" -I"$LIB/src" \ "$CI_DIR/reset_freq/t_reset_freq.cpp" "$CI_DIR/stubs/Wire.cpp" "$LIB/src/si5351.cpp" \