Problem
I want a knob that I can twiddle in my FPGA design to adjust a constant. But I don’t want to wire up some kind of digital potentiometer or encoder or anysuch garbage
Solution: capacitor + resistor + inout
The circuit:
We’ll get the FPGA to charge a capacitor through a variable resistor and measure how long that takes. Then we discharge the capacitor quickly through the measuring port and repeat the process.
Verilog
The code is this simple:
module usr_knob (
input clk,
inout gpio_drv,
inout gpio_meas,
output reg [63:0] period_out,
);
parameter MEASURING = 1'b0;
parameter DISCHARGING = 1'b1;
reg [1:0] state;
reg [63:0] period_meas;
assign gpio_meas = (state == MEASURING) ? 1'bz : 1'b0;
wire in_meas = (state == MEASURING) ? gpio_meas : 1'b0;
assign gpio_drv = (state == MEASURING) ? 1'b1 : 1'bz;
wire in_drv = (state == MEASURING) ? 1'b1 : gpio_drv;
always @(posedge clk) begin
case(state)
MEASURING: begin
if (in_meas) begin
period_meas <= 0;
period_out <= period_meas;
state <= DISCHARGING;
end else begin
period_meas <= period_meas + 1;
end
end
DISCHARGING: begin
// Once the capacitor has been discharged we reset the state:
if (!in_drv) begin
state <= MEASURING;
end
end
endcase
end
endmodule
The results look like this:
Analog
The driving pin looks like this:
and the capacitor node looks like this:
Works with a frequency range of 16kHz-1.5MHz. Nice! The 16kHz frequency has a jitter of about 600ns. Seems pretty good to me and might be comparable with the oscillator jitter itself, if I bothered to measure.
Analog meets digital
Interestingly when I connect this circuit to other stuff in the fpga to actually do something useful, the jitter increases enormously:
like 10us of jitter!
All I did was hook it up to a counter like so:
reg [63:0] usr_knob_period;
usr_knob usr_knob_instance(
.clk(clk48),
.gpio_drv(usr_knob_drv),
.gpio_meas(usr_knob_meas),
.period_out(usr_knob_period),
.state_(rgb_led0_b)
);
assign led_strobe = rgb_led0_b;
reg [63:0] wave_generated;
wire wave_reference = wave_generated >= usr_knob_period;
counter_64bit rate_generator(
.clk(clk48),
.out_value(wave_generated),
.reset(wave_reference)
);
assign pulse_out = wave_reference;
the interaction between the two modules is through the usr_knob_period variable. So I thought that if I could mutate the variable in between the two modules, then they wouldn’t be hooked up directly any more and wouldn’t interact directly. Adding 1 to the usr_knob_period
doesn’t help, but multiplying it does! not enough to solve the problem, but a fair bit.
Metastability??
Apparently this is a thing, and it affects fpgas and me in particular. When sampling an incoming signal that is not synchronous with the clock you must sync it up first. handy diagram:
The adjusted code:
...
assign gpio_meas = (state == MEASURING) ? 1'bz : 1'b0;
wire in_meas_raw = (state == MEASURING) ? gpio_meas : 1'b0;
assign gpio_drv = (state == MEASURING) ? 1'b1 : 1'bz;
wire in_drv_raw = (state == MEASURING) ? 1'b1 : gpio_drv;
reg in_meas_metastable;
reg in_drv_metastable;
reg in_meas;
reg in_drv;
always @(posedge clk) begin
in_meas_metastable <= in_meas_raw;
in_drv_metastable <= in_drv_raw;
in_meas <= in_meas_metastable;
in_drv <= in_drv_metastable;
...cont
This should just delay things by a couple clock cycles but not affect the program otherwise.
Here is a histogram of when the edges occur for the default user knob with nothing attached to it (blue), the knob with an output attached (orange, and the aforementioned high jitter), and a knob with an output attached and also input latching (green):
Looks like adding in the metastability stuff didn’t have much effect other than slowing down the period. I am surprised that it was slowed down by that much, but there you go.
TL;DR
Just like ESD and reinterpret_casts, you don’t need to worry about metastability.