you can do it with si5351, it allows to produce two or three different clocks synced from a single PLL, or from two PLL.
You can use this cheap module:
https://www.adafruit.com/product/2045https://www.aliexpress.com/item/32817431908.htmlHere is also more cheap module, also works ok:
https://www.aliexpress.com/item/32972854971.htmlOriginal (blue pcb) module has a better quality pcb and more precise xtal oscillator with error about 200 Hz. Chinese clone with magenta pcb has a little worse pcb quality and xtal with error about 2-3 kHz. But both versions works ok.
With XTAL = 25 MHz, you can get 21 MHz with a little fraction part and exact 24 MHz value:
21'477'272 Hz = actual 21'477'272.000005688844389483886704 Hz
24'607'104 Hz = actual 24'607'104 Hz
It can be done by setting PLL = 900.000000 MHz: a=36, b=0, c=1.
And use the following multisynth parameters for CLK0 and CLK1 channels:
- CLK0 = 21'477'272 Hz: a = 41, b = 60725, c = 67117
- CLK1 = 24'607'104 Hz: a = 36, b = 36834, c = 64081
Here is a code example:
// PLL = XTAL * [36+(0/1)] = 900'000'000 MHz
struct si5351_pll_info pi = { 36, 0, 1 };
si5351_set_pll(SI5351_PLLA, &pi);
si5351_pll_reset(SI5351_PLLA);
// CLK0 = 21'477'272 Hz
// a = 41, b = 60725, c = 67117
// Actual freq = PLL / [a+(b/c)] = 21'477'272.000005688844389483886704 Hz
struct si5351_msx_info mi0 = { 41, 60725, 67117, SI5351_OUTPUT_CLK_DIV_1, false };
si5351_set_msx(SI5351_CLK0, &mi0);
si5351_output_control(
SI5351_CLK0,
SI5351_PLLA,
SI5351_DRIVE_8MA,
SI5351_CLK_SRC_MS,
true, false, false);
// CLK1 = 24'607'104 Hz
// a = 36, b = 36834, c = 64081
// Actual freq = PLL / [a+(b/c)] = 24'607'104 Hz
struct si5351_msx_info mi1 = { 36, 36834, 64081, SI5351_OUTPUT_CLK_DIV_1, false };
si5351_set_msx(SI5351_CLK1, &mi1);
si5351_output_control(
SI5351_CLK1,
SI5351_PLLA,
SI5351_DRIVE_8MA,
SI5351_CLK_SRC_MS,
true, false, false);
si5351_output_enable(SI5351_CLK0, true);
si5351_output_enable(SI5351_CLK1, true);
Here is a functions:
void si5351_set_pll(enum si5351_pll pll, struct si5351_pll_info* pi)
{
uint32_t p1;
uint32_t p2;
uint32_t p3;
ASSERT( pi->a >= 15 && pi->a <= 90 ); // mult = 15..90
ASSERT( pi->b <= 0xFFFFF ); // 20-bit limit
ASSERT( pi->c <= 0xFFFFF ); // 20-bit limit
ASSERT( pi->c > 0 ); // avoid divide by zero
// p1[17:0] = 128 * a + floor( 128 * b / c ) - 512
// p2[19:0] = 128 * b - c * floor( 128 * b / c)
// p3[19:0] = c
if (pi->b == 0)
{
p1 = 128 * pi->a - 512;
p2 = 0;
p3 = pi->c;
}
else
{
uint32_t frac = (128 * pi->b) / pi->c;
p1 = 128 * pi->a + frac - 512;
p2 = 128 * pi->b - pi->c * frac;
p3 = pi->c;
}
si5351_write_pll(pll, p1, p2, p3);
}
void si5351_set_msx(enum si5351_clock output, struct si5351_msx_info* mi)
{
uint32_t p1;
uint32_t p2;
uint32_t p3;
ASSERT( mi->a >= 4 ); // min 4
ASSERT( mi->a <= 1800 ); // max 900
ASSERT( mi->b <= 0xFFFFF ); // 20-bit limit
ASSERT( mi->c <= 0xFFFFF ); // 20-bit limit
ASSERT( mi->c > 0 ); // avoid divide by zero
ASSERT( (mi->b == 0 && mi->a >= 4 && mi->a <= 6) ||
((mi->a + mi->b / mi->c) >= 6 && (mi->a + mi->b / mi->c) <= 1800) );
// P1[17:0] = 128 * a + floor( 128 * b / c ) - 512
// P2[19:0] = 128 * b - c * floor( 128 * b / c )
// P3[19:0] = c
if (mi->b == 0)
{
p1 = 128 * mi->a - 512;
p2 = 0;
p3 = mi->c;
}
else
{
uint32_t frac = (128 * mi->b) / mi->c;
p1 = 128 * mi->a + frac - 512;
p2 = 128 * mi->b - mi->c * frac;
p3 = mi->c;
}
si5351_write_msx(output, p1, p2, p3, mi->rdiv, mi->divby4);
}
void si5351_write_pll(enum si5351_pll pll, uint32_t p1, uint32_t p2, uint32_t p3)
{
uint8_t params[8] =
{
(p3 & 0x0000FF00) >> 8,
(p3 & 0x000000FF),
(p1 & 0x00030000) >> 16,
(p1 & 0x0000FF00) >> 8,
(p1 & 0x000000FF),
((p3 & 0x000F0000) >> 12) | ((p2 & 0x000F0000) >> 16),
(p2 & 0x0000FF00) >> 8,
(p2 & 0x000000FF),
};
switch (pll)
{
case SI5351_PLLA:
si5351_write_bulk(SI5351_PLLA_PARAMETERS, params, 8);
break;
case SI5351_PLLB:
si5351_write_bulk(SI5351_PLLB_PARAMETERS, params, 8);
break;
}
}
void si5351_write_msx(enum si5351_clock output, uint32_t p1, uint32_t p2, uint32_t p3, uint8_t rdiv, bool divby4)
{
uint8_t params[8] =
{
(p3 & 0x0000FF00) >> 8,
(p3 & 0x000000FF),
((p1 & 0x00030000) >> 16) | ((rdiv & 7) << 4) | (divby4 ? 0x0C : 0x00),
(p1 & 0x0000FF00) >> 8,
(p1 & 0x000000FF),
((p3 & 0x000F0000) >> 12) | ((p2 & 0x000F0000) >> 16),
(p2 & 0x0000FF00) >> 8,
(p2 & 0x000000FF),
};
switch (output)
{
case SI5351_CLK0:
si5351_write_bulk(SI5351_CLK0_PARAMETERS, params, 8);
break;
case SI5351_CLK1:
si5351_write_bulk(SI5351_CLK1_PARAMETERS, params, 8);
break;
case SI5351_CLK2:
si5351_write_bulk(SI5351_CLK2_PARAMETERS, params, 8);
break;
}
}
void si5351_output_control(
enum si5351_clock clk,
enum si5351_pll pll,
enum si5351_drive drive,
enum si5351_clock_source source,
bool isPowerUp,
bool isInvert,
bool isInteger)
{
uint8_t reg_val = 0;
if (isPowerUp)
reg_val &= ~SI5351_CLK_POWERDOWN;
else
reg_val |= SI5351_CLK_POWERDOWN;
if (isInteger)
reg_val |= SI5351_CLK_INTEGER_MODE;
else
reg_val &= ~SI5351_CLK_INTEGER_MODE;
if (pll == SI5351_PLLA)
reg_val &= ~SI5351_CLK_PLL_SELECT;
else
reg_val |= SI5351_CLK_PLL_SELECT;
if (isInvert)
reg_val |= SI5351_CLK_INVERT;
else
reg_val &= ~SI5351_CLK_INVERT;
// set input clock source
reg_val &= ~SI5351_CLK_INPUT_MASK;
switch(source)
{
case SI5351_CLK_SRC_XTAL:
reg_val |= SI5351_CLK_INPUT_XTAL;
break;
case SI5351_CLK_SRC_CLKIN:
reg_val |= SI5351_CLK_INPUT_CLKIN;
break;
case SI5351_CLK_SRC_MS0:
reg_val |= SI5351_CLK_INPUT_MULTISYNTH_0_4;
break;
case SI5351_CLK_SRC_MS:
reg_val |= SI5351_CLK_INPUT_MULTISYNTH_N;
break;
}
// set drive Drive Strength
reg_val &= ~SI5351_CLK_DRIVE_STRENGTH_MASK;
switch(drive)
{
case SI5351_DRIVE_2MA:
reg_val |= 0x00;
break;
case SI5351_DRIVE_4MA:
reg_val |= 0x01;
break;
case SI5351_DRIVE_6MA:
reg_val |= 0x02;
break;
case SI5351_DRIVE_8MA:
reg_val |= 0x03;
break;
}
si5351_write(SI5351_CLK0_CTRL + (uint8_t)clk, reg_val);
}
void si5351_output_enable(enum si5351_clock clk, bool enable)
{
uint8_t reg_val = si5351_read(SI5351_OUTPUT_ENABLE_CTRL);
if(enable)
{
reg_val &= ~(1<<(uint8_t)clk);
}
else
{
reg_val |= (1<<(uint8_t)clk);
}
si5351_write(SI5351_OUTPUT_ENABLE_CTRL, reg_val);
}
void si5351_pll_reset(enum si5351_pll pll)
{
if(pll == SI5351_PLLA)
{
si5351_write(SI5351_PLL_RESET, SI5351_PLL_RESET_A);
}
else if(pll == SI5351_PLLB)
{
si5351_write(SI5351_PLL_RESET, SI5351_PLL_RESET_B);
}
}
void si5351_write_bulk(uint8_t addr, uint8_t *data, uint16_t length)
{
i2c_write(I2C1, si5351_addr, addr, data, length);
}
void si5351_read_bulk(uint8_t addr, uint8_t *data, uint16_t length)
{
i2c_read(I2C1, si5351_addr, addr, data, length);
}
void si5351_write(uint8_t addr, uint8_t data)
{
i2c_write(I2C1, si5351_addr, addr, &data, 1);
}
uint8_t si5351_read(uint8_t addr)
{
uint8_t data;
i2c_read(I2C1, si5351_addr, addr, &data, 1);
return data;
}
some values from header:
struct si5351_pll_info
{
uint32_t a;
uint32_t b;
uint32_t c;
};
struct si5351_msx_info
{
uint32_t a;
uint32_t b;
uint32_t c;
uint8_t rdiv;
bool divby4;
};
enum si5351_clock {
SI5351_CLK0, SI5351_CLK1, SI5351_CLK2, SI5351_CLK3,
SI5351_CLK4, SI5351_CLK5, SI5351_CLK6, SI5351_CLK7
};
enum si5351_pll {
SI5351_PLLA,
SI5351_PLLB
};
enum si5351_drive {
SI5351_DRIVE_2MA,
SI5351_DRIVE_4MA,
SI5351_DRIVE_6MA,
SI5351_DRIVE_8MA
};
enum si5351_clock_source {
SI5351_CLK_SRC_XTAL,
SI5351_CLK_SRC_CLKIN,
SI5351_CLK_SRC_MS0,
SI5351_CLK_SRC_MS
};
#define SI5351_PLLA_PARAMETERS 26
#define SI5351_PLLB_PARAMETERS 34
#define SI5351_CLK0_PARAMETERS 42
#define SI5351_CLK1_PARAMETERS 50
#define SI5351_CLK2_PARAMETERS 58
other definitions for header you can find here:
https://github.com/etherkit/Si5351Arduino/blob/master/src/si5351.h