A bit relevant data about the project
I'm working on a project that includes sampling a 10 MHz analogue signal at around 60 Msps on an ADS4222 (2 12 bit channels). The clock is generated using the ICE40UP5K's internal PLL (jitter doesn't matter that much for the chosen application) and an external TXCO. Data sampling on the FPGA is triggered using the ADS's output clock and stores data into a FIFO structure. All data readings following the write operation sequentially read the stored data. The SPI protocol is currently very simple to reduce the timing footprint. SPI write messages trigger the conversion, while SPI reads simply read the data one by one. First bit in the message specifies the operation.
This is the verilog source:
- core.v (connects all the components, instantiates the PLL core)
- ram.v (RAM and FIFO implementation)
- spi.v (async SPI core - counter is 5-bit)
The problem
When I sample the data, the FIFO counter counts fine (it stops sampling after the expected time), but no matter the source, whether it's a counter for debugging purposes or the actual ADC data, it doesn't appear to actually sample the correct data. Now if I set it to a constant, it seems to sample it into the FIFO just fine - when reading the data via SPI, I always get the same SPI bit stream as expected. This leads me to believe that there is something wrong with timing - whether it's on the read or write side of things since static data seems to work fine.
The 24 bit samples are interpreted as 2x12 signed integers. If I have it set up to read from a counter (should be sequential numbers) I get something like this:
If limited to 4 samples (read in pairs, so data appears to be changing mid readout?):
Short readout:
-1868,-1869,176,52,-1866,-1867,176,54
If I use larger sample buffers (let's say the full length of 4096) I get what appears to be random samples filled with a bunch of zeroes:
A longer readout:
What I confirmed is working
- The async SPI core is working: if I load registry data and read it on the connected MCU, I get correct readings.
- Parallel data lines are wired correctly: sampling the current reading in a registry at the time a new SPI message starts successfully sends a sample pair via SPI directly - when I sample a sine wave I get the corresponding bathtub histogram
- The ADS conversion done clock is wired correctly: If I time the "busy" line on an oscilloscope I get roughly the time of 4096 clock cycles
- The configuration works correctly when simulated in ModelSim, so I'm assuming it's something with timing (simulated with data depth of 4 measurements):
A simulated sampling operation:
A simulated sampling operation
A simulated readout operation via SPI:
A simulated readout operation via SPI The sample timing should also be correct, at least in theory, according to the TI ADS42xx family's datasheet (figure 7-1): ADS4222 timing
- I'm not near the EBR ram max speed either as per the ICE40 family datasheet ICE40UP5K EBR timing
I tried playing around with this structure a lot in the past few weeks and had no luck, so I'm wondering if anyone had similar issues. I'm new to FPGAs in general so I'm sure it could be that I missed something completely generic and stupid, so I decided to ask in this community if anyone is willing to share their experience.
Thank you in advance!
1 Answer 1
That has got to be some of the strangest FIFO code I've ever seen. I'm not even going to try to debug it; it really needs a complete rewrite.
Muxing the clocks to drive a single counter makes no sense at all.
Given that you have separate write and read clocks, and separate write and read ports on your RAM, it would make a whole lot more sense to have one process for writing and one process for reading, each with its own address counter, and a simple pair of signals that allow each one to signal the other when it reaches its maximum count — with appropriate clock-domain-crossing precautions on those signals.
Since I'm currently on a train with nothing better to do, here's what I would suggest. First, here's a generic module that I use for getting a signal across a CDC, xd_sync.v. It also does edge detection in the destination domain, providing pulses for rising, falling or both edges.
module xd_sync #(
/* number of resync FFs before edge detector */
parameter STAGES = 3
) (
input clock,
input din,
output dout,
output dout_d,
output any_edge,
output rising,
output falling
);
/* The ASYNC_REG property is specific to CDCs on Xliinx parts */
(* ASYNC_REG = "TRUE" *) reg [STAGES:0] din_d;
always @(posedge clock) begin
din_d <= {din_d[STAGES-1:0], din};
end
assign dout = din_d[STAGES-1];
assign dout_d = din_d[STAGES];
assign any_edge = dout ^ dout_d;
assign rising = dout && !dout_d;
assign falling = !dout && dout_d;
endmodule
(Note that the code from here on is completely untested, but I believe it should work.)
Second, let's define an address counter that should work for either writing or reading. Note that by making RESET_STATE a parameter, we can control which counter is active following a reset.
module fifo_address #(
/* address width */
parameter WIDTH = 12,
/* state of "done" on reset */
parameter RESET_STATE = 0
) (
input clock,
input reset,
input start,
input enable,
output [WIDTH-1:0] address,
output done
);
/* Extra bit in counter serves as "done" state */
reg [WIDTH:0] counter;
wire [WIDTH:0] reset_value = RESET_STATE ? {1'b1 {WIDTH-1{1'b0}} : 0;
wire start_pulse;
/* Handle "start" as an asynchronous input, looking for a rising edge */
xd_sync xd_sync_1 (
.clock (clock),
.din (start),
.dout (),
.dout_d (),
.any_edge (),
.rising (start_pulse),
.falling ()
);
always @(posedge clock) begin
if (reset || start_pulse) begin
counter <= reset_value;
end else if (enable && !counter[WIDTH]) begin
counter <= counter + 1;
end
end
assign address = counter[WIDTH-1:0];
assign done = counter[WIDTH];
endmodule
Now we can define the top-level dual-clock ping-pong FIFO module. Note that this is much simpler than a fully asynchronous dual-clock FIFO would be. For that, you'd probably be better off using a FIFO generated by your vendor's tools.
module dc_pingpong_fifo #(
parameter addr_width = 12,
parameter data_width = 24,
parameter max_data_count = (1<<addr_width)
) (
input rst,
input write_en,
input wclk,
input [data_width-1:0] din,
input rclk,
output [data_width-1:0] dout,
output full,
output empty,
output direction // 0 is write, 1 is read (not actually used)
);
wire [addr_width-1:0] waddr;
wire w_done;
wire [addr_width-1:0] raddr;
wire r_done;
// RAM module for the FIFO
ram #(
.addr_width (addr_width),
.data_width (data_width)
) ram_inst (
.din (din),
.write_en (write_en),
.waddr (waddr),
.wclk (wclk),
.raddr (raddr),
.rclk (rclk),
.dout (dout)
);
/* Write address generator */
fifo_address #(
.WIDTH (addr_width),
.RESET_STATE (0)
) fifo_address_write (
.clock (wclk),
.reset (rst),
.start (r_done),
.enable (write_en),
.address (waddr),
.done (w_done)
);
/* Read address generator */
fifo_address #(
.WIDTH (addr_width),
.RESET_STATE (1)
) fifo_address_read (
.clock (rclk),
.reset (rst),
.start (w_done),
.enable (dir),
.address (raddr),
.done (r_done)
);
/* Direction logic */
assign full = w_done;
assign empty = r_done;
assign direction = !r_done;
endmodule