Skip to content

Si5351C refactoring - #1747

Merged
mossmann merged 4 commits into
greatscottgadgets:mainfrom
martinling:si5351c-defs
May 18, 2026
Merged

Si5351C refactoring#1747
mossmann merged 4 commits into
greatscottgadgets:mainfrom
martinling:si5351c-defs

Conversation

@martinling

@martinling martinling commented May 2, 2026

Copy link
Copy Markdown
Member

This PR refactors the Si5351C driver code. The goal is to make it more maintainable and easier to add new features, by depending less on hand-written bit wrangling.

  1. Although this PR is primarily an internal refactor, I've made a couple of changes to the external API used by clock_gen.c in order to better name and distinguish different concepts and types. These can be made independently of the refactoring work so are placed first.

  2. Next, we add a new si5351c_regs.def file with register definitions and helper functions. This is similar to our other def files for other register-based parts, but with additional macros that simplify access to fields which are split across multiple registers, and for indexed access to fields which are duplicated for each multisynth, or for each PLL.

  3. Finally, the implementation in si5351.c is rewritten to take advantage of the new helpers, while retaining the same structure and external API. As with other register-based parts, we now maintain a cached copy of the registers and a regs_dirty bitmask, and flush changes in a si5351c_regs_commit() function. The commit implementation will group writes to sequences of adjacent registers, so performance should match the previous hand-written code.

@martinling
martinling force-pushed the si5351c-defs branch 2 times, most recently from 15ef8db to 9fa07ca Compare May 2, 2026 22:50
@martinling martinling changed the title Si5351C Si5351C refactoring May 2, 2026
@martinling
martinling force-pushed the si5351c-defs branch 5 times, most recently from d98a294 to 4d8e4ff Compare May 3, 2026 08:35
@martinling

Copy link
Copy Markdown
Member Author

This has been tested on HackRF One and Praline in the HITL CI, and also verified locally by taking diffs of hackrf_debug register dumps before and after the changes.

There is one remaining difference in my register dumps:

 [180] -> 0x00
 [181] -> 0x30
 [182] -> 0x1f
-[183] -> 0x80
+[183] -> 0x92
 [184] -> 0x60
 [185] -> 0x60
 [186] -> 0xb8

Previously, we were writing 0x80 to register 183, which sets the crystal load capacitance in the top two bits, and sets the rest of the register to zero. However, the documentation specifies that the reserved lower bits should be written as 010010:

image

The new implementation does this, because it reads the reserved bits during setup and retains them when updating the XTAL_CL field later.

@martinling
martinling marked this pull request as ready for review May 3, 2026 09:02
@martinling

Copy link
Copy Markdown
Member Author

There is another difference in register dumps after this change, which happens specifically on Praline:

 [ 16] -> 0x0d
 [ 17] -> 0x0c
 [ 18] -> 0x0c
-[ 19] -> 0xc0
+[ 19] -> 0xe0
 [ 20] -> 0x7d
 [ 21] -> 0x6d
 [ 22] -> 0xc0

This corresponds to MS3_SRC changing from 0 to 1, meaning that CLKOUT is now using PLL B instead of PLL A.

This is because of what I assume is a bug in the IS_PRALINE branch in si5351c_configure_clock_control, which previously did:

/* CLK3: CLKOUT */
clkout_ctrl = SI5351C_CLK_INT_MODE | SI5351C_CLK_PLL_SRC(SI5351C_PLL_B)  |
        SI5351C_CLK_SRC(SI5351C_CLK_SRC_MULTISYNTH_SELF) |
        SI5351C_CLK_IDRV(SI5351C_CLK_IDRV_8MA);

...but never copied the new value of clkout_ctrl to the data array which was about to be sent over I2C. So the change to clkout_ctrl, which differed only in the PLL selection, never took effect.

I have assumed that the intent was to use PLL B for CLKOUT on Praline, so I now have:

/* CLK3: CLKOUT */
clkout.pll = SI5351C_PLL_B;
clk[3] = clkout;

@martinling
martinling marked this pull request as draft May 3, 2026 12:10
@martinling

Copy link
Copy Markdown
Member Author

Something is still wrong with this on HackRF One r9; achieved sample rate appears to be around 14MHz (with XTAL) or 15MHz (with CLKIN) when it should be 10MHz.

After startup and a hackrf_transfer -r /dev/null run, I get the following register differences before and after this PR:

@@ -1,4 +1,4 @@
-[  0] -> 0x11
+[  0] -> 0x71
 [  1] -> 0xf8
 [  2] -> 0x03
 [  3] -> 0xfc
@@ -17,9 +17,9 @@
 [ 16] -> 0x4e
 [ 17] -> 0x0d
 [ 18] -> 0xc0
-[ 19] -> 0x80
-[ 20] -> 0x80
-[ 21] -> 0x80
+[ 19] -> 0xc0
+[ 20] -> 0xc0
+[ 21] -> 0xc0
 [ 22] -> 0xc0
 [ 23] -> 0xc0
 [ 24] -> 0x00
@@ -220,8 +220,8 @@
 [219] -> 0x00
 [220] -> 0x00
 [221] -> 0x00
-[222] -> 0x00
-[223] -> 0x40
+[222] -> 0x94
+[223] -> 0x28
 [224] -> 0x00
 [225] -> 0x00
 [226] -> 0x38

Register 0 is the status register, and is indicating loss of lock on both PLLs, which would certainly explain the wrong sample rate, but I don't know why.

Registers 19-21 should be irrelevant; these are the clock control registers for the CLK3-CLK5 outputs, which don't exist on the Si5351A, and are also flagged as powered down.

Registers 222-223 are undocumented, and listed as reserved in AN619, so I've no idea what that change means.

@martinling

Copy link
Copy Markdown
Member Author

Found the problem on r9 - just a dumb mistake in the first commit, when updating the logic controlling h1r9_clkin_en.

@martinling
martinling marked this pull request as ready for review May 5, 2026 16:53
@martinling
martinling requested a review from mossmann May 5, 2026 16:53
@mossmann

Copy link
Copy Markdown
Member

Previously, we were writing 0x80 to register 183, which sets the crystal load capacitance in the top two bits, and sets the rest of the register to zero. However, the documentation specifies that the reserved lower bits should be written as 010010:

I did some testing and found that some Si5351Cs installed on H1s have those low bits set to 0 at start-up. I also found that changing those bits has no effect on clock frequency.

@mossmann mossmann left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great overall! I found one small bug.

Comment thread firmware/common/si5351c.h Outdated
There are multiple similar but different things called a "source"
or "clock source", or that are set in a register called "SRC", in the
context of the Si5351 and its usage in HackRF.

One place we can be unambiguous is that there are only two inputs to
the Si5351C itself: XTAL and CLKIN. Let's have a type for that alone,
and name it as such when we use it.

There should be no functional changes in this commit.
We're going to want a type that just identifies one PLL, and is
consistent with how the PLLs are numbered in register settings.

The current si5351c_pll_t is a bitmask which allows referring to
both PLLs. Let's make that a different type, and name things more
clearly when using it.
@mossmann
mossmann merged commit c77e7e7 into greatscottgadgets:main May 18, 2026
50 checks passed
@Brumi-2021

Copy link
Copy Markdown

Hi @martinling and @mossmann, first of all, congratulations on your great work across the GSG projects and on the excellent documentation.

In relation to this PR, I would like to share some investigation results from the Mayhem/PortaPack side, after observing a HackRF USB mode regression on one out of four HackRF devices I tested.

Apologies for the long post, but I wanted to include the experimental details and hardware results.

While investigating a HackRF USB mode regression affecting one particular HackRF One r6 + PortaPack H2 (WM8731), we eventually traced the issue back to the Si5351C changes introduced by this PR.

The affected unit works correctly as a PortaPack, but in HackRF USB mode
with applications such as SDR++ and GQRX it exhibits incorrect RF
reception: USB communication and IQ streaming continue to work, tuning
commands are accepted, and LNA/VGA/RF AMP controls work, but the received
spectrum is incorrect/mistuned and the expected RF signal cannot be
received correctly.

The investigation originally started as this Mayhem issue:

portapack-mayhem/mayhem-firmware#3268

After extensive hardware testing and manual bisecting, we narrowed the
regression boundary to these two consecutive Mayhem nightlies:

nightly 2026-05-21 -> GOOD
nightly 2026-05-22 -> BAD

on the affected device.

The corresponding HackRF submodule revisions are:

May21 GOOD:
  Mayhem: 865bdee9b37dcb5ac344db242035645f4bd1bc06
  HackRF: 0bf478ed259410a5cef085b6a50c795a80c096ad

May22 BAD:
  Mayhem: f3aee44e3b9a09ec41bb46c8135fe85eae4b9562
  HackRF: f526ddf4ebba5fda5ad0075c78238ea0ce1cc02a

After isolating the HackRF changes in that interval, the regression was
further narrowed to commit:

016910567bc236fcfb14ebb741185c468416cda8

from this PR.

More specifically, we were eventually able to isolate the physical
regression to the handling of Si5351 register 183.

Before this change, HackRF programmed REG183 deterministically as:

REG183 = 0x80

The refactored driver first snapshots the Si5351 register map and uses
cached read/modify/write operations. For REG183, set_XTAL_CL() changes
D7:D6 while preserving D5:D0 from the cached value.

During the investigation we noticed the earlier comment in this PR about
REG183:

Previously, we were writing 0x80 to register 183 [...]

and, in particular, the observation that some Si5351Cs installed on H1s
had the low bits set to zero at startup and that changing those bits did
not appear to affect clock frequency.

Our hardware testing has produced a different and reproducible result on
one r6/H2 unit.

Skyworks AN619 Rev. 0.8 specifies REG183 as:

D7:D6 = XTAL_CL[1:0]
D5:D0 = Reserved

and states that D5:D0 should be written as:

010010b = 0x12

Therefore, with the existing 8 pF XTAL_CL setting:

D7:D6 = 10b
D5:D0 = 010010b

giving:

REG183 = 0x92

This also matches what we observe in current HackRF firmware:

hackrf_debug --si5351c -n 183 -r
[183] -> 0x92

However, on the affected r6/H2 device, REG183=0x92 reproducibly causes
the RF failure, while REG183=0x80 reproducibly restores correct RF
operation.

We isolated this experimentally while retaining the new register
cache/dirty/commit architecture.

Historical isolated tests on the affected device:

REG183 low bits preserved -> BAD-STREAMING-RF-WRONG
REG183 forced to 0x80     -> GOOD
REG183 forced to 0x92     -> BAD-STREAMING-RF-WRONG

The 0x80 and 0x92 test candidates used the same helper/cache/dirty/commit
path, the same initialization position and the same I2C transaction.
Only REG183 D5:D0 differed.

We then repeated the experiment on current Mayhem next / current HackRF.

Current upstream base:

Mayhem:
ca9ca93fa29c745d6550c6ad41c58479e5a8963a

HackRF:
749bca5f10c491906fbb1acc6fb466784b73ef43

Current next is BAD on the affected device and reports:

[183] -> 0x92

We then made a one-line diagnostic change in the current Si5351 driver:

void si5351c_set_crystal_configuration(si5351c_driver_t* const drv)
{
    set_XTAL_CL(drv, SI5351C_XTAL_8PF);
    drv->regs[183] &= 0xC0;
    si5351c_regs_commit(drv);
}

This keeps the current register cache/dirty/commit implementation intact
and only makes D5:D0 deterministic zero again, producing exactly:

REG183 = 0x80

No I2C timing, clock source, PLL, multisynth, startup sequence or other
Si5351 configuration was changed.

The corresponding current-next test revisions are:

Mayhem:
1e55ade8896c76a8e8aa20eb2cbedfe919941c1f

HackRF:
1b7774135280ed2c73215e090a7921324d7fea8f

The firmware was built using the normal Mayhem Docker toolchain.

SHA256 of the tested HackRF One firmware:

8c354a79c4aa0acc0de2cfd33ff834134992ceeac61b43a19ff63e75d90b4b2c

SHA256 of the tested HackRF Pro firmware:

05f4703a9fe6212f3a50ea25597035c19b857e8a633222cec688425795add5c1

On the affected device this immediately changes the result from:

current next + REG183 0x92 -> BAD
current next + REG183 0x80 -> GOOD

We then tested the same current-next 0x80 candidate across four different
physical devices:

Device 1:
  HackRF One r6 + PortaPack H2 + WM8731
  Previously affected
  REG183 = 0x80
  Result: GOOD

Device 2:
  HackRF One r6 + PortaPack H1 + AK4951K
  Previously unaffected
  REG183 = 0x80
  Result: GOOD

Device 3:
  HackRF One r9 + PortaPack H2 + WM8731
  Previously unaffected
  REG183 = 0x80
  Result: GOOD

Device 4:
  HackRF Pro + PortaPack H4M + WM8731
  Previously unaffected
  REG183 = 0x80
  Result: GOOD

In all four cases the programmed value was also verified directly using:

hackrf_debug --si5351c -n 183 -r
[183] -> 0x80

So the current-next validation result is 4/4 GOOD with deterministic
REG183=0x80.

For comparison, REG183=0x92 is not generally broken: it works correctly
on the other tested hardware. For example, the r6/H1 and HackRF Pro both
operate correctly with 0x92.

Therefore we are NOT suggesting that AN619 is generally wrong, nor that
0x92 is generally invalid.

What we have found is at least one reproducible hardware case where:

REG183 = 0x92 -> incorrect RF operation
REG183 = 0x80 -> correct RF operation

while USB communication and IQ streaming themselves remain operational.

This particular affected HackRF One r6/H2 is an older third-party unit,
so a silicon/BOM/device variation is certainly possible. We have not
identified the underlying electrical reason why D5:D0 affect this unit
while apparently being benign on the others.

For reference, here is a photo of the Si5351 device fitted on the affected HackRF One r6/H2 PCB. The package marking is visible in the photo.
image

Given the earlier testing mentioned in this PR, we would be very
interested in your interpretation:

Could there be a known Si5351C silicon/BOM/compatible-device variation,
or an interaction with the 25 MHz reference implementation, that could
explain why REG183=0x92 is benign on most tested units but reproducibly
breaks RF operation on this particular r6/H2, while restoring the
historical REG183=0x80 behavior fixes it?

We can provide the individual test commits / firmware hashes and further
hardware measurements if useful.

@miek

miek commented Sep 1, 2026

Copy link
Copy Markdown
Member

One possible difference comes to mind: during the chip shortages a few years ago some third-party vendors decided to substitute in Si5351A instead of Si5351C on the boards they produced. Unfortunately there's no way to tell from the package marking which variant is fitted (the 'A' is for the chip revision not the model variant).

However, since the Si5351A has no CLKIN support, they also added an extra footprint breaking the CLKIN PCB trace, so you could check to see if that is present. There are some pictures of that on issue #1099 . I do notice your board has the same marking & date code as the one in that issue, so I think it's pretty likely it's one of those boards.

@Brumi-2021

Brumi-2021 commented Sep 1, 2026

Copy link
Copy Markdown

Thanks @miek, that's a very interesting observation.
Looking again at my PCB photo, the additional CLKIN footprint shown in #1099 appears to be present in approximately the same location, although unfortunately it is partially hidden by the P16 connector, so I cannot confirm its exact population/connection without reopening the unit.

image

I would prefer not to reopen it at the moment because this old board has some damaged solder-mask areas that I have had to insulate carefully.

Your observation about the identical package marking/date code is particularly interesting. This could indeed mean that the affected r6 board belongs to that batch of third-party boards with a possible Si5351A substitution.

One additional data point is that my Device 3 is a HackRF One r9, therefore using the Si5351A design, and it works correctly with both REG183=0x92 and our 0x80 test. So Si5351A alone would not seem sufficient to explain the REG183 sensitivity, but perhaps the combination of the substituted device and this particular r6/third-party board design is relevant.

image

From REG0, this gives:

Device 1: REVID[1:0] = 0
Device 2: REVID[1:0] = 1
Device 3: REVID[1:0] = 1
Device 4: REVID[1:0] = 1

I understand that REVID identifies the silicon revision rather than the Si5351 A/B/C variant, so I am not using this as evidence that Device 1 contains an Si5351A. However, it may be another useful clue: the affected board is also the only device in my four-unit sample reporting a different Si5351 revision.

Also, Device 3 (known Si5351A design) and Device 4 (Si5351C) both return exactly REG0 = 0x11, which seems to confirm that REG0 itself cannot be used to distinguish the A/C variants.

@martinling

Copy link
Copy Markdown
Member Author

We don't know what the reserved bits of register 183 are. However, in general the Si5351 register map is very logical and related settings are placed together, so I suspect that those bits also relate to the crystal oscillator circuit somehow. They could be adjusting drive strength of the output for the driven side of the crystal, for instance.

It would be interesting to probe pin 2 (XB) of the Si5351 on the troublesome board to see how the crystal waveform differs between the 0x80 and 0x92 settings.

@Brumi-2021

Copy link
Copy Markdown

Thanks @martinling , that is a very interesting possibility.

The idea that REG183 D5:D0 may control some undocumented analogue parameter of the oscillator block would fit the reproducible 0x80 / 0x92 behaviour we are seeing.

I will try tomorrow to probe XB (pin 2) on the affected board and compare the waveform with REG183=0x80 and REG183=0x92.

One detail worth mentioning is that Device 1 appears to use a passive 4-pad 25 MHz crystal, so I will first verify the exact XA/XB connection before interpreting the measurement.

In any case, experimentally we have already established that this particular Device 1 cannot operate correctly with external SDR programs (sdr++, gqrx ,....) with REG183=0x92, while restoring the previous REG183=0x80 value makes it work correctly and reproducibly.

Once we have the oscilloscope measurements, I would therefore appreciate considering whether some form of backward-compatibility detection could be appropriate for these older/special hardware variants, rather than changing the current behaviour for all devices.

As a preliminary example only, one possibility might be to use the Si5351 REVID value: the affected device is the only one in my four-device sample reporting REVID=0, while all three unaffected devices report REVID=1. This could potentially allow something like 0x80 for REVID=0 devices and the current AN619 0x92 behaviour for REVID>=1.

Of course, at this point we only have one REVID=0 device, so I would not assume that this is the correct detection criterion without further evidence. I mention it only as an example of the kind of backward-compatible solution that might be possible.

@martinling

Copy link
Copy Markdown
Member Author

It's worth noting that those REVID=0 parts are very old.

Even this photo of a Jawbreaker board from 2012 already shows an Si5351 with the B marking, i.e. REVID=1, so presumably the REVID=0 parts could have been manufactured no later than mid-2012. They must have lurked on a shelf for a decade until the 2021-2022 chip shortage, when those clones with the broken CLKIN appeared.

The Si5351 family appear to have been first marketed in late 2010, so it may be that REVID=0 are just the parts from the first year or two of production - but it's also possible that Rev. 0 was for pre-production samples, in which case the advice in AN619 might not even be intended to apply to them.

@gullradriel

Copy link
Copy Markdown
Contributor

Yeah, some of our users (including some of us maintainers) are having old devices. I don't see a nice way to tell it, but we are trying to keep compatibility even with old devices.

@martinling

Copy link
Copy Markdown
Member Author

I've opened PR #1811, which implements the proposal to zero the reserved bits of register 183 for rev 0 parts, whilst following the advice in AN619 otherwise.

It's possible that the reason this helps is not related to the Si5351 revision, but rather to something about the crystal used on that specific clone board. However, even if that's the case, this could still be the right solution. Since the rev 0 chips predate the entire HackRF project, it's quite possible that this one batch of chip-shortage-era clones, with Si5351A and broken CLKIN, are the only HackRF boards that will ever exist with rev 0 chips on them.

What's still not clear is whether 0x80 is the best choice of register value for those boards - perhaps the crystal has a different load capacitance, or perhaps zero is not the best value for the mystery bits. You could investigate that if you want to, but 0x80 seems to work, so if you're happy with that we can go with it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants