August Puzzle: The Mysterious Lock Loss
— SCENARIO: You’re debugging a Costas loop implementation that works
— perfectly in simulation but fails intermittently in hardware.
— The loop locks quickly to F1 (carrier + 1kHz), but when the
— input switches to F2 (carrier + 3kHz), it sometimes loses lock
— entirely instead of reacquiring.
—
— PUZZLE: What’s causing this intermittent lock loss?
— HINT: Look carefully at the loop filter characteristics and gain scheduling.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
use IEEE.MATH_REAL.ALL;
entity costas_loop_puzzle is
generic (
DATA_WIDTH : integer := 16;
PHASE_WIDTH : integer := 12;
F1_OFFSET : integer := 1000; — 1 kHz offset from carrier
F2_OFFSET : integer := 3000 — 3 kHz offset from carrier
);
port (
clk : in std_logic;
reset : in std_logic;
rf_input : in signed(DATA_WIDTH-1 downto 0);
freq_select : in std_logic; — ‘0’ for F1, ‘1’ for F2
— Outputs for debugging
i_data : out signed(DATA_WIDTH-1 downto 0);
q_data : out signed(DATA_WIDTH-1 downto 0);
phase_error : out signed(DATA_WIDTH-1 downto 0);
vco_freq : out signed(PHASE_WIDTH-1 downto 0);
lock_detect : out std_logic
);
end entity;
architecture behavioral of costas_loop_puzzle is
— VCO signals
signal vco_phase : signed(PHASE_WIDTH-1 downto 0) := (others => ‘0’);
signal vco_i, vco_q : signed(DATA_WIDTH-1 downto 0);
signal vco_control : signed(DATA_WIDTH-1 downto 0) := (others => ‘0’);
— Mixer outputs
signal mixer_i, mixer_q : signed(DATA_WIDTH-1 downto 0);
— Loop filter components
signal integrator : signed(DATA_WIDTH+4-1 downto 0) := (others => ‘0’);
signal proportional : signed(DATA_WIDTH-1 downto 0);
signal error_signal : signed(DATA_WIDTH-1 downto 0);
— Lock detection
signal error_magnitude : unsigned(DATA_WIDTH-1 downto 0);
signal lock_counter : unsigned(15 downto 0) := (others => ‘0’);
— Critical parameters (this is where the puzzle lies!)
constant KP : signed(7 downto 0) := to_signed(32, 8); — Proportional gain
constant KI : signed(7 downto 0) := to_signed(2, 8); — Integral gain
— Gain scheduling based on frequency (THE TRAP!)
signal adaptive_kp : signed(7 downto 0);
signal adaptive_ki : signed(7 downto 0);
begin
— Gain scheduling logic – reduces gains at higher frequencies
— This looks reasonable but creates the error starvation!
process(freq_select)
begin
if freq_select = ‘0’ then — F1 mode
adaptive_kp <= KP;
adaptive_ki <= KI;
else — F2 mode – “optimize” for stability at higher frequency
adaptive_kp <= shift_right(KP, 2); — KP/4
adaptive_ki <= shift_right(KI, 3); — KI/8
end if;
end process;
— VCO phase accumulator
process(clk, reset)
begin
if reset = ‘1’ then
vco_phase <= (others => ‘0’);
elsif rising_edge(clk) then
vco_phase <= vco_phase + vco_control;
end if;
end process;
— VCO sine/cosine generation (simplified)
— In real implementation, this would be a lookup table
vco_i <= to_signed(integer(32767.0 * cos(real(to_integer(vco_phase)) * MATH_PI / 2048.0)), DATA_WIDTH);
vco_q <= to_signed(integer(32767.0 * sin(real(to_integer(vco_phase)) * MATH_PI / 2048.0)), DATA_WIDTH);
— Quadrature mixers
process(clk)
begin
if rising_edge(clk) then
— Multiply and low-pass filter (simplified)
mixer_i <= shift_right(rf_input * vco_i, 15);
mixer_q <= shift_right(rf_input * vco_q, 15);
end if;
end process;
— Costas loop error detector (classic I*sign(Q) approach)
process(clk)
variable q_sign : signed(DATA_WIDTH-1 downto 0);
begin
if rising_edge(clk) then
if mixer_q >= 0 then
q_sign := to_signed(1, DATA_WIDTH);
else
q_sign := to_signed(-1, DATA_WIDTH);
end if;
error_signal <= shift_right(mixer_i * q_sign, 8);
end if;
end process;
— Loop filter with adaptive gains
process(clk, reset)
variable scaled_error : signed(DATA_WIDTH+4-1 downto 0);
variable prop_term : signed(DATA_WIDTH+4-1 downto 0);
begin
if reset = ‘1’ then
integrator <= (others => ‘0’);
vco_control <= (others => ‘0’);
elsif rising_edge(clk) then
— Scale error by adaptive gains
scaled_error := resize(error_signal * adaptive_ki, DATA_WIDTH+4);
prop_term := resize(error_signal * adaptive_kp, DATA_WIDTH+4);
— Integrate with adaptive gain
integrator <= integrator + scaled_error;
— PI controller output
vco_control <= resize(shift_right(integrator + prop_term, 4), DATA_WIDTH);
end if;
end process;
— Lock detector – measures error magnitude
process(clk, reset)
begin
if reset = ‘1’ then
lock_counter <= (others => ‘0’);
lock_detect <= ‘0’;
elsif rising_edge(clk) then
error_magnitude <= unsigned(abs(error_signal));
if error_magnitude < 100 then — Low error threshold
if lock_counter < 65535 then
lock_counter <= lock_counter + 1;
end if;
else
lock_counter <= (others => ‘0’);
end if;
— Declare lock after 1000 consecutive low-error samples
if lock_counter > 1000 then
lock_detect <= ‘1’;
else
lock_detect <= ‘0’;
end if;
end if;
end process;
— Output assignments
i_data <= mixer_i;
q_data <= mixer_q;
phase_error <= error_signal;
vco_freq <= resize(vco_control, PHASE_WIDTH);
end behavioral;