diff --git a/.github/workflows/flash-budget.yml b/.github/workflows/flash-budget.yml index 148e1a3..19ff175 100644 --- a/.github/workflows/flash-budget.yml +++ b/.github/workflows/flash-budget.yml @@ -1,8 +1,10 @@ -# 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. +# Uno flash ceilings for the minimal sketches, including min_runtime compiled +# with -DSI5351_NO_CLK67. 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. The gated build is this same job, not a +# separate workflow. name: Uno flash budget on: diff --git a/README.md b/README.md index 3cd6dbe..9663e02 100644 --- a/README.md +++ b/README.md @@ -294,6 +294,33 @@ Constraints * 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 +Leaving out CLK6 and CLK7 +------------------------- +If your sketch never uses CLK6 or CLK7, you can leave their code out and save about 1 KB of flash on an AVR board. In the Arduino IDE, put the define above the include in your sketch's .ino file: + +```cpp +#define SI5351_NO_CLK67 +#include +``` + +Put it in the .ino file only. If more than one file in the sketch defines it before including `si5351.h`, the link fails with a duplicate definition. The define has to come before the `#include`. + +With the opt-out, `set_freq()` and `set_freq_manual()` return 1 for CLK6 and CLK7 and write nothing, and `set_ms()` writes nothing for CLK6 and CLK7. The public methods and the `si5351_clock` enumerators stay, and `reset()` still initializes all eight clock-control registers. Without the define, behavior is unchanged. + +How it works: the CLK6/CLK7 functions in `si5351.cpp` are weak symbols. The sketch define makes `si5351.h` add small stubs that replace them, and the linker drops the originals. This needs a GCC or Clang toolchain, which every common Arduino core uses. + +You can also pass the macro as a compiler definition. This leaves the code out when `si5351.cpp` is compiled: + +* Arduino CLI: `--build-property compiler.cpp.extra_flags=-DSI5351_NO_CLK67` +* PlatformIO: `build_flags = -DSI5351_NO_CLK67` + +Measured flash on an Arduino Uno (arduino-cli 1.5.1, arduino:avr 1.8.6), in bytes: + +| Sketch | Default | Sketch `#define` | Compiler flag | +|---|---|---|---| +| `init()` + one `set_freq()`, runtime clock index | 10,868 | 9,870 | 9,836 | +| `init()` + one `set_freq()` on CLK0 | 10,140 | 9,778 | 9,740 | + 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. diff --git a/src/si5351.cpp b/src/si5351.cpp index f1fbb42..6e748ac 100644 --- a/src/si5351.cpp +++ b/src/si5351.cpp @@ -26,6 +26,7 @@ #include "Arduino.h" #include "Wire.h" +#define SI5351_LIBRARY_SOURCE #include "si5351.h" @@ -61,7 +62,9 @@ static uint8_t recalc_force; // 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) +// Left out of the build when SI5351_NO_CLK67 is defined (see the README). +#ifndef SI5351_NO_CLK67 +SI5351_CLK67_WEAK uint8_t Si5351::ms67_div(uint64_t pll_freq, uint64_t freq, uint8_t *r_div) { uint64_t n; uint8_t r; @@ -92,6 +95,7 @@ static uint8_t ms67_div(uint64_t pll_freq, uint64_t freq, uint8_t *r_div) return 0; } +#endif // 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. @@ -196,22 +200,21 @@ bool Si5351::init(uint8_t xtal_load_c, uint32_t xo_freq, int32_t corr) } si5351_write(SI5351_CRYSTAL_LOAD, xtal_load_c | 0b00010010); - // Set up the XO and CLKIN reference frequencies - if (xo_freq != 0) + // Set up the XO and CLKIN reference frequencies, then the + // calibrations. Both references are stored before either + // correction replans a PLL. One loop so this stays a single + // copy when init() is inlined into main. + uint32_t ref = xo_freq != 0 ? xo_freq : (uint32_t)SI5351_XTAL_FREQ; + uint8_t r; + for(r = 0; r < 2; r++) { - set_ref_freq(xo_freq, SI5351_PLL_INPUT_XO); - set_ref_freq(xo_freq, SI5351_PLL_INPUT_CLKIN); //Also CLKIN + set_ref_freq(ref, (enum si5351_pll_input)r); } - else + for(r = 0; r < 2; r++) { - set_ref_freq(SI5351_XTAL_FREQ, SI5351_PLL_INPUT_XO); - set_ref_freq(SI5351_XTAL_FREQ, SI5351_PLL_INPUT_CLKIN); //Also CLKIN + set_correction(corr, (enum si5351_pll_input)r); } - // 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; @@ -236,34 +239,30 @@ void Si5351::reset(void) // 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) + // 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), then turn them back on. 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++) + for(i = 0; i < 2; i++) { - set_ms_source((enum si5351_clock)i, SI5351_PLLA); + set_pll(SI5351_PLL_FIXED, (enum si5351_pll)i); } - for(i = 6; i < 8; i++) + + // Make PLL to CLK assignments for automatic tuning. + // CLK0-5 on PLLA, CLK6-7 on PLLB. set_ms_source updates pll_assignment[]. + for(i = 0; i < 8; i++) { - set_ms_source((enum si5351_clock)i, SI5351_PLLB); + set_ms_source((enum si5351_clock)i, (i < 6) ? SI5351_PLLA : SI5351_PLLB); } // Turn off spread spectrum (SSC_EN, reg 149 bit 7). Some chips power up @@ -272,14 +271,17 @@ void Si5351::reset(void) 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); + // Reset the VCXO param (registers 162..164) + for(i = 0; i < 3; i++) + { + si5351_write(SI5351_VXCO_PARAMETERS_LOW + i, 0); + } // Then reset the PLLs - pll_reset(SI5351_PLLA); - pll_reset(SI5351_PLLB); + for(i = 0; i < 2; i++) + { + pll_reset((enum si5351_pll)i); + } // Set initial frequencies for(i = 0; i < 8; i++) @@ -488,92 +490,107 @@ uint8_t Si5351::set_freq(uint64_t freq, enum si5351_clock clk) } else { - // MS6 and MS7 logic - // ----------------- - uint8_t peer = (clk == SI5351_CLK6) ? (uint8_t)SI5351_CLK7 : (uint8_t)SI5351_CLK6; + return set_freq67(freq, clk); + } +} - // There is no valid MS6/7 setting for 0 Hz - if(freq == 0) - { - return 1; - } +#ifndef SI5351_NO_CLK67 +// set_freq() for CLK6/CLK7. Weak: a sketch that defines SI5351_NO_CLK67 +// before #include replaces it with a stub (see si5351.h). +SI5351_CLK67_WEAK uint8_t Si5351::set_freq67(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; - // Lower bounds check (lowest valid MS6/7 ratio, VCO and R combination) - if(freq < SI5351_CLKOUT67_MIN) - { - freq = SI5351_CLKOUT67_MIN; - } + // MS6 and MS7 logic + // ----------------- + uint8_t peer = (clk == SI5351_CLK6) ? (uint8_t)SI5351_CLK7 : (uint8_t)SI5351_CLK6; - // Upper bounds check - if(freq >= SI5351_MULTISYNTH_DIVBY4_FREQ * SI5351_FREQ_MULT) - { - freq = SI5351_MULTISYNTH_DIVBY4_FREQ * SI5351_FREQ_MULT - 1; - } + // There is no valid MS6/7 setting for 0 Hz + if(freq == 0) + { + return 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; + // Lower bounds check (lowest valid MS6/7 ratio, VCO and R combination) + if(freq < SI5351_CLKOUT67_MIN) + { + freq = SI5351_CLKOUT67_MIN; + } - // Set the freq in memory - clk_freq[(uint8_t)clk] = freq; - } - else - { - // No previous assignment, so set PLLB based on this clock + // Upper bounds check + if(freq >= SI5351_MULTISYNTH_DIVBY4_FREQ * SI5351_FREQ_MULT) + { + freq = SI5351_MULTISYNTH_DIVBY4_FREQ * SI5351_FREQ_MULT - 1; + } - // Set the freq in memory - clk_freq[(uint8_t)clk] = freq; + // 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; - // 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++; - } + // Set the freq in memory + clk_freq[(uint8_t)clk] = freq; + } + else + { + // No previous assignment, so set PLLB based on this clock - 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; - } + // Set the freq in memory + clk_freq[(uint8_t)clk] = freq; - // 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) + // Smallest R divider that keeps the MS6/7 ratio <= 254 + while(freq < SI5351_MS67_MIN_FREQ && r_div < SI5351_OUTPUT_CLK_DIV_128) { - output_enable(clk, 1); - clk_first_set[(uint8_t)clk] = true; + freq <<= 1; + r_div++; } - div_by_4 = 0; - int_mode = 0; + 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; + } - // Set multisynth registers (MS must be set before PLL) - set_ms(clk, ms_reg, int_mode, r_div, div_by_4); + div_by_4 = 0; + int_mode = 0; - // 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]); - } + // Set multisynth registers (MS must be set before PLL) + set_ms(clk, ms_reg, int_mode, r_div, div_by_4); - return 0; + // 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; } +#endif /* * set_freq_manual(uint64_t freq, uint64_t pll_freq, enum si5351_clock clk) @@ -615,6 +632,7 @@ uint8_t Si5351::set_freq_manual(uint64_t freq, uint64_t pll_freq, enum si5351_cl // R divider, or refuse before writing anything if there is none if((uint8_t)clk >= (uint8_t)SI5351_CLK6) { +#ifndef SI5351_NO_CLK67 ms_reg.p1 = ms67_div(pll_freq, freq, &r_div); if(ms_reg.p1 == 0) { @@ -622,6 +640,9 @@ uint8_t Si5351::set_freq_manual(uint64_t freq, uint64_t pll_freq, enum si5351_cl } ms_reg.p2 = 0; ms_reg.p3 = 0; +#else + return 1; +#endif } else if(freq >= SI5351_MULTISYNTH_DIVBY4_FREQ * SI5351_FREQ_MULT) @@ -692,27 +713,18 @@ void Si5351::set_pll(uint64_t pll_freq, enum si5351_pll target_pll) { struct Si5351RegSet pll_reg; uint8_t params[SI5351_PARAMETERS_LENGTH]; + // PLLB keeps c = 10^6 while the VCXO is in use (AN619 sec. 3.2, 7). + // set_vcxo() stores the pull range and calls back here, so this path + // is the only copy of pll_calc / pack / write / pll_update. + uint8_t vcxo = (target_pll == SI5351_PLLB && vcxo_pull) ? 1 : 0; + enum si5351_pll_input ref = (target_pll == SI5351_PLLA) ? plla_ref_osc : pllb_ref_osc; - // 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)])) + if(!ref_ok(xtal_freq[(uint8_t)ref])) { 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); - } + uint64_t vcxo_param = pll_calc(target_pll, pll_freq, &pll_reg, ref_correction[ref], vcxo); // Derive the register values to write pack_reg_set(&pll_reg, params, 0); @@ -720,6 +732,22 @@ void Si5351::set_pll(uint64_t pll_freq, enum si5351_pll target_pll) // Write the parameters si5351_write_bulk((target_pll == SI5351_PLLA) ? SI5351_PLLA_PARAMETERS : SI5351_PLLB_PARAMETERS, SI5351_PARAMETERS_LENGTH, params); + + if(vcxo) + { + uint8_t temp; + uint8_t ppm = vcxo_pull; + + 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(target_pll, pll_freq); } @@ -759,6 +787,7 @@ void Si5351::pll_update(enum si5351_pll pll, uint64_t pll_freq) r_div = 0; if(i >= 6) { +#ifndef SI5351_NO_CLK67 reg.p1 = ms67_div(pll_freq, f, &r_div); if(reg.p1 == 0) { @@ -767,6 +796,9 @@ void Si5351::pll_update(enum si5351_pll pll, uint64_t pll_freq) reg.p2 = 0; reg.p3 = 0; div_by_4 = 0; +#else + continue; +#endif } else { @@ -833,14 +865,32 @@ void Si5351::set_ms(enum si5351_clock clk, struct Si5351RegSet ms_reg, uint8_t i } 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); + set_ms67(clk, (uint8_t)ms_reg.p1, r_div); + } +} + +#ifndef SI5351_NO_CLK67 +// set_ms() for CLK6/CLK7. Weak, like set_freq67(). +SI5351_CLK67_WEAK void Si5351::set_ms67(enum si5351_clock clk, uint8_t p1, uint8_t r_div) +{ + // MS6 and MS7 only use one register (90 / 91). + si5351_write(SI5351_CLK6_PARAMETERS + ((uint8_t)clk - (uint8_t)SI5351_CLK6), p1); + + // R6_DIV is register 92 bits 2:0, R7_DIV bits 6:4 + uint8_t reg_val = si5351_read(SI5351_CLK6_7_OUTPUT_DIVIDER); + if(clk == SI5351_CLK6) + { + reg_val = (reg_val & ~(0x07)) | r_div; + } + else + { + reg_val = (reg_val & ~(0x70)) | (r_div << SI5351_OUTPUT_CLK_DIV_SHIFT); } + si5351_write(SI5351_CLK6_7_OUTPUT_DIVIDER, reg_val); } +#endif /* * output_enable(enum si5351_clock clk, uint8_t enable) @@ -957,13 +1007,19 @@ void Si5351::update_status(void) * should not have to be done again for the same Si5351 and * crystal. */ +__attribute__((noinline)) void Si5351::set_correction(int32_t corr, enum si5351_pll_input ref_osc) { + uint8_t p; + 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); + // Recalculate and set PLL freqs based on correction value. + // PLLA then PLLB, same order as the two direct calls. + for(p = 0; p < 2; p++) + { + set_pll(p ? pllb_freq : plla_freq, (enum si5351_pll)p); + } } /* @@ -1371,9 +1427,6 @@ void Si5351::set_pll_input(enum si5351_pll pll, enum si5351_pll_input input) */ 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) { @@ -1390,34 +1443,10 @@ void Si5351::set_vcxo(uint64_t pll_freq, uint8_t ppm) return; } - // Remember the VCXO setting so later PLLB updates keep c = 10^6 + // Remember the VCXO setting so later PLLB updates keep c = 10^6. + // set_pll() writes the PLL and the VCXO parameter registers. 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_pll(pll_freq, SI5351_PLLB); } /* @@ -1548,15 +1577,10 @@ uint8_t Si5351::get_i2c_error(void) 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; - } + // One reference selection for both PLLs. The two arms used to each carry + // their own copy of the feedback math once this function was inlined. + enum si5351_pll_input ref_osc = (pll == SI5351_PLLA) ? plla_ref_osc : pllb_ref_osc; + uint64_t ref_freq = xtal_freq[(uint8_t)ref_osc] * SI5351_FREQ_MULT; //ref_freq = 15974400ULL * SI5351_FREQ_MULT; uint32_t a, b, c; uint64_t lltmp; //, denom; @@ -1590,42 +1614,34 @@ uint64_t Si5351::pll_calc(enum si5351_pll pll, uint64_t freq, struct Si5351RegSe 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; + // Find best approximation for b/c = fVCO mod fIN. + // RFRAC_DENOM is 1000000, the same denominator set_vcxo() needs so + // PLLB keeps c = 10^6 (AN619 sec. 3.2). One division serves both. + b = (((uint64_t)(freq % ref_freq)) * RFRAC_DENOM) / ref_freq; if(vcxo) { - b = (((uint64_t)(freq % ref_freq)) * 1000000ULL) / ref_freq; - c = 1000000ULL; + c = RFRAC_DENOM; } 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); + if(vcxo) + { + return (uint64_t)(128 * a * RFRAC_DENOM + b); + } + // 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; - } + return freq; } uint64_t Si5351::multisynth_calc(uint64_t freq, uint64_t pll_freq, struct Si5351RegSet *reg) @@ -1722,6 +1738,7 @@ uint64_t Si5351::multisynth_calc(uint64_t freq, uint64_t pll_freq, struct Si5351 } } +#ifndef SI5351_NO_CLK67 uint64_t Si5351::multisynth67_calc(uint64_t freq, uint64_t pll_freq, struct Si5351RegSet *reg) { //uint8_t p1; @@ -1810,6 +1827,7 @@ uint64_t Si5351::multisynth67_calc(uint64_t freq, uint64_t pll_freq, struct Si53 } } } +#endif void Si5351::update_sys_status(struct Si5351Status *status) { @@ -1846,51 +1864,24 @@ void Si5351::update_int_status(struct Si5351IntStatus *int_status) 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; + // CLK0..CLK5 only (CLK6/7 are in set_ms67()): parameter byte 2 is + // linear with clk + uint8_t reg_addr = SI5351_CLK0_PARAMETERS + 2 + ((uint8_t)clk * 8); + uint8_t reg_val = si5351_read(reg_addr); - 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); + // Clear the relevant bits + reg_val &= ~(0x7c); - if((uint8_t)clk <= (uint8_t)SI5351_CLK5) + if(div_by_4 == 0) { - // 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); + reg_val &= ~(SI5351_OUTPUT_CLK_DIVBY4); } - else if(clk == SI5351_CLK6) + else { - // Clear the relevant bits - reg_val &= ~(0x07); - - reg_val |= r_div; + reg_val |= (SI5351_OUTPUT_CLK_DIVBY4); } - else if(clk == SI5351_CLK7) - { - // Clear the relevant bits - reg_val &= ~(0x70); - reg_val |= (r_div << SI5351_OUTPUT_CLK_DIV_SHIFT); - } + reg_val |= (r_div << SI5351_OUTPUT_CLK_DIV_SHIFT); si5351_write(reg_addr, reg_val); } diff --git a/src/si5351.h b/src/si5351.h index b7552d0..d809350 100644 --- a/src/si5351.h +++ b/src/si5351.h @@ -355,7 +355,11 @@ class Si5351 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 *); + // CLK6/CLK7 code. See SI5351_NO_CLK67 at the end of this file. uint64_t multisynth67_calc(uint64_t, uint64_t, struct Si5351RegSet *); + uint8_t set_freq67(uint64_t, enum si5351_clock); + static uint8_t ms67_div(uint64_t, uint64_t, uint8_t *); + void set_ms67(enum si5351_clock, uint8_t, uint8_t); void update_sys_status(struct Si5351Status *); void update_int_status(struct Si5351IntStatus *); void ms_div(enum si5351_clock, uint8_t, uint8_t); @@ -369,4 +373,20 @@ class Si5351 uint8_t vcxo_pull; }; +/* Leaving out the CLK6/CLK7 code (about 1 KB of flash on an Uno). + * Put this at the top of the sketch, in one file only (the .ino): + * #define SI5351_NO_CLK67 + * #include + * or pass -DSI5351_NO_CLK67 to the compiler. set_freq() and + * set_freq_manual() on CLK6/CLK7 then return 1 and write nothing, and + * set_ms() on CLK6/CLK7 writes nothing. + * The library's CLK6/CLK7 functions are weak; the stubs below replace + * them, and the linker drops the originals. */ +#define SI5351_CLK67_WEAK __attribute__((weak)) +#if defined(SI5351_NO_CLK67) && !defined(SI5351_LIBRARY_SOURCE) +uint8_t Si5351::set_freq67(uint64_t, enum si5351_clock) { return 1; } +uint8_t Si5351::ms67_div(uint64_t, uint64_t, uint8_t *) { return 0; } +void Si5351::set_ms67(enum si5351_clock, uint8_t, uint8_t) {} +#endif + #endif /* SI5351_H_ */ diff --git a/tests/regression/README.md b/tests/regression/README.md index 5045400..5dd4298 100644 --- a/tests/regression/README.md +++ b/tests/regression/README.md @@ -53,6 +53,6 @@ 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/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 · +`ctrl/` `plan/` branch locks · `flash/check_flash_budget.sh` Uno byte ceilings, including the `SI5351_NO_CLK67` compiler-flag and sketch-#define builds (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/flash/budgets.txt b/tests/regression/flash/budgets.txt index 98b91a2..4a41930 100644 --- a/tests/regression/flash/budgets.txt +++ b/tests/regression/flash/budgets.txt @@ -12,13 +12,16 @@ # 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 +# v2.4.2 baselines on this pin: min_const 10830, min_runtime 11558. +# 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_runtime is a global volatile uint8_t clock index and a constant 14 MHz. +# min_runtime_no_clk67 is that program compiled with -DSI5351_NO_CLK67. +# Ceilings below are the sizes after the PLL and init/reset merge, so a +# later edit cannot grow back to the v2.4.2 baseline. +min_const 10140 +min_runtime 10868 +min_runtime_no_clk67 9836 +# min_runtime_sketch_no_clk67: same program with #define SI5351_NO_CLK67 in the +# sketch (no compiler flag). A library that ignores the define builds 10930. +min_runtime_sketch_no_clk67 9870 diff --git a/tests/regression/flash/check_flash_budget.sh b/tests/regression/flash/check_flash_budget.sh index 92161cc..5a21509 100755 --- a/tests/regression/flash/check_flash_budget.sh +++ b/tests/regression/flash/check_flash_budget.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# Fail-closed Uno flash budget for the two minimal v2.4.2 sketches. +# Fail-closed Uno flash budget for the minimal sketches, including the +# SI5351_NO_CLK67 build of the runtime-clock sketch. # 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 @@ -66,6 +67,7 @@ gate_log() { build_one() { local name="$1" + local extra="${2:-}" 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)" @@ -73,10 +75,15 @@ build_one() { 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 + local -a cmd=("$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") + # Optional second property, used to pass -DSI5351_NO_CLK67 into si5351.cpp. + # A #define in the .ino does not reach the library translation unit. + if [[ -n "$extra" ]]; then + cmd+=(--build-property "$extra") + fi + if ! "${cmd[@]}" "$sk" > "$log" 2>&1; then fail "$name did not compile (see $log)" fi local elf="$OUT/build/$name/${name}.ino.elf" @@ -100,9 +107,11 @@ build_one() { echo "$name $sketch_bytes ${BUDGET[$name]} $text $data $bss" >> "$OUT/measured.tsv" } -for name in min_const min_runtime; do - build_one "$name" -done +build_one min_const +build_one min_runtime +build_one min_runtime_no_clk67 "compiler.cpp.extra_flags=-DSI5351_NO_CLK67" +# Sketch-side opt-out: the #define is in the .ino, no compiler flag. +build_one min_runtime_sketch_no_clk67 # 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 diff --git a/tests/regression/flash/sketches/min_runtime_no_clk67.ino.txt b/tests/regression/flash/sketches/min_runtime_no_clk67.ino.txt new file mode 100644 index 0000000..ea97063 --- /dev/null +++ b/tests/regression/flash/sketches/min_runtime_no_clk67.ino.txt @@ -0,0 +1,19 @@ +#include "si5351.h" +#include "Wire.h" + +Si5351 si5351; + +/* Same program as min_runtime.ino.txt. The flash budget checker compiles + * this sketch with -DSI5351_NO_CLK67 so multisynth67_calc and the CLK6/CLK7 + * branches are left out of si5351.cpp. This covers the compiler-flag + * opt-out; min_runtime_sketch_no_clk67 covers the #define-in-sketch one. + */ +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/flash/sketches/min_runtime_sketch_no_clk67.ino.txt b/tests/regression/flash/sketches/min_runtime_sketch_no_clk67.ino.txt new file mode 100644 index 0000000..81b5ec1 --- /dev/null +++ b/tests/regression/flash/sketches/min_runtime_sketch_no_clk67.ino.txt @@ -0,0 +1,23 @@ +/* min_runtime.ino.txt with the sketch-side opt-out: SI5351_NO_CLK67 is + * defined here, before the include, and NOT passed to the compiler. The + * weak CLK6/CLK7 functions are replaced by the header's stubs. */ +#define SI5351_NO_CLK67 +#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/python/__pycache__/si5351_math.cpython-313.pyc b/tests/regression/python/__pycache__/si5351_math.cpython-313.pyc new file mode 100644 index 0000000..afde029 Binary files /dev/null and b/tests/regression/python/__pycache__/si5351_math.cpython-313.pyc differ diff --git a/tests/regression/python/__pycache__/spec_crosscheck.cpython-313.pyc b/tests/regression/python/__pycache__/spec_crosscheck.cpython-313.pyc new file mode 100644 index 0000000..08ae402 Binary files /dev/null and b/tests/regression/python/__pycache__/spec_crosscheck.cpython-313.pyc differ diff --git a/tests/regression/python/__pycache__/spec_oracle.cpython-313.pyc b/tests/regression/python/__pycache__/spec_oracle.cpython-313.pyc new file mode 100644 index 0000000..51dd974 Binary files /dev/null and b/tests/regression/python/__pycache__/spec_oracle.cpython-313.pyc differ