As configuring the PLL is such an important part of the project I want to explain it in more detail. What I am going to describe here is specific to the FPGA used in this project but the general principles can certainly be transferred to other FPGAs.
Configuring the PLL
The following discussion refers to Altera’s MAX10 PLL and especially to the ALTPLL_RECONFIG Ip core. For details go see MAX10 Clocking and PLL User Guide.
The PLL’s behavior can be configured by setting these parameters
- Pre-scale counter (N)
- Feedback counter (M)
- Post-scale output counters (C0–C4)
- Charge pump current (ICP)
- Loop filter components (R, C
The PLL Reconfiguration Scan Chain
These parameters are embedded in a structure called PLL Reconfiguration Scan Chain. In C this structure can be described like so.
struct __attribute__((packed))PllChain
{
int c4_lowcount:8;
int c4_odd :1;
int c4_highcount:8;
int c4_bypass :1;
int c3_lowcount:8;
int c3_odd :1;
int c3_highcount:8;
int c3_bypass :1;
int c2_lowcount:8;
int c2_odd :1;
int c2_highcount:8;
int c2_bypass :1;
int c1_lowcount:8;
int c1_odd :1;
int c1_highcount:8;
int c1_bypass :1;
int c0_lowcount:8;
int c0_odd :1;
int c0_highcount:8;
int c0_bypass :1;
int m_lowcount:8;
int m_odd :1;
int m_highcount:8;
int m_bypass :1;
int n_lowcount:8;
int n_odd :1;
int n_highcount:8;
int n_bypass :1;
int cp_current:3;
int cp_reserved:5;
int vcopostscale :1;
int lf_resistance:5;
int lf_capacitance:2;
int lfreserved:2;
};
For our purposes we only need the three parameters m, n and c to set the main PLL output frequency. To write these values into the scan chain we can use this function.
bool configurePll(uint8_t m, uint8_t n, uint8_t c )
{
FPGA::PllChain pllchain;
memset (&pllchain, 0, sizeof(FPGA::PllChain));
// default configuration 200MHz
pllchain.c4_bypass = 1;
pllchain.c3_bypass = 1;
pllchain.c2_bypass = 1;
pllchain.c1_bypass = 1;
pllchain.c0_lowcount = c/2;
pllchain.c0_highcount = (c+1)/2;
pllchain.c0_odd = (c & 1) ? 1 : 0;
pllchain.c0_bypass = (c == 1) ? 1 : 0;
pllchain.m_lowcount = m/2;
pllchain.m_highcount = (m+1)/2;
pllchain.m_odd = (m & 1) ? 1 :0 ;
pllchain.m_bypass = (m == 1) ? 1 : 0;
pllchain.n_lowcount = n/2;
pllchain.n_highcount = (n+1)/2;
pllchain.n_odd = (n & 1) ? 1 : 0;
pllchain.n_bypass = (n == 1) ? 1 : 0;
pllchain.cp_current = 1; // 1
pllchain.lf_resistance = 27; // 27
pllchain.lf_capacitance = 0; // 0
return FPGA::configurePll(&pllchain);
}
Before we continue to the FPGA we need to answer the question of how to determine m, n and c for a given frequency. For this we set up a reference table which lists all possible PLL frequencies with their corresponding values for m, n and c. We can then look up the values in this table.
Computing the PLL table
struct __attribute__((packed)) PLLtable
{
uint16_t f10kHz;
uint8_t m;
uint8_t n;
uint8_t c;
};
constexpr PLLtable plltable[]
{
{12000,12,1,5},
{12009,245,17,6},
{12012,185,11,7},
{12013,173,9,8},
…
};
Such a table can be computed using the following Python skript.
VCOMAX = 1e9
VCOMIN = 599.9e6
M_MAX = 255
N_MAX = 255
C_MAX = 255
F0 = 50e6
FMAX = 370e6
FMIN = 120e6
combinations = {}
def computeCombinations():
with open(“plltable.txt”, “w”) as file:
for m in range(1, M_MAX+1):
for n in range(1, N_MAX+1):
fVCO = F0 * m / n
if fVCO > VCOMAX or fVCO < VCOMIN:
continue
for c in range(1, C_MAX+1):
f = fVCO / c
if f >= FMIN and f <= FMAX:
f10kHz = int(f/10000)
try:
if combinations[f10kHz]:
pass
except:
combinations[f10kHz] = (m,n,c)
file.write(f”{f10kHz}, {m}, {n}, {c}\n”)
Writing the Scan Chain into the PLL
On the FPGA we define some registers and instatiate the IP core
wire pll_clk;
wire pll_locked;
reg pll_areset = 0;
reg pll_configupdate = 0;
reg pll_scanclkena = 0;
wire pll_scandata;
wire pll_scandataout;
wire pll_scandone;
// PLL control
reg [7:0] pllchain [17:0];
reg [4:0] pllchainbyteread = 0;
reg [2:0] pllchainbitwrite = 0;
reg [4:0] pllchainbytewrite = 0;
reg pllwritestart = 0;
reg pllsavedrunningstate = 0;
assign pll_scandata = pllchain[pllchainbytewrite][pllchainbitwrite];
PLL200MHzRec PLL200MHzRec_inst (
.areset ( pll_areset ),
.configupdate ( pll_configupdate ),
.inclk0 ( clk ),
.scanclk ( clk ),
.scanclkena ( pll_scanclkena ),
.scandata ( pll_scandata ),
.c0 ( pll_clk ),
.locked ( pll_locked ),
.scandataout ( pll_scandataout ),
.scandone ( pll_scandone )
);
After the command interpreter has received the last byte of the scan chain from the MCU and put it into pllchain it asserts pllwritestart. This activates the task below which transfers the scan chain into the PLL.
reg [1:0] delayscandata = 0;
// Program PLL scanchain into the device
always @(negedge clk ) begin
if (pllwritestart) begin
pll_scanclkena <= 1;
if (delayscandata < 2) begin
delayscandata <= delayscandata + 2’d1;
end else begin
pllchainbitwrite <= pllchainbitwrite + 3’d1;
if (pllchainbitwrite == 3’d7) begin
pllchainbytewrite <= pllchainbytewrite + 5’d1;
if (pllchainbytewrite == 5’d17)
pll_scanclkena <= 0;
end
end
end else begin
delayscandata <= 0;
pllchainbitwrite <= 0;
pllchainbytewrite <= 0;
end
end
After the transfer is complete and the IP core asserts pll_scandone we have to pulse pll_areset to finish the reconfiguration.