Skip to content

Instantly share code, notes, and snippets.

@jkominek
Last active June 15, 2024 04:40
Show Gist options
  • Save jkominek/1b8e16f663ae75a4ed081de1361232cc to your computer and use it in GitHub Desktop.
Save jkominek/1b8e16f663ae75a4ed081de1361232cc to your computer and use it in GitHub Desktop.
FTDI 232H synchronous FIFO fed by a on-FPGA clock domain crossing FIFO, in Verilog
// Asynchronous FIFO module
module async_fifo #(
// Parameters
parameter DATA_SIZE = 8, // Number of data bits
parameter ADDR_SIZE = 4 // Number of bits for address
) (
// Inputs
input [DATA_SIZE-1:0] w_data, // Data to be written to FIFO
input w_en, // Write data and increment addr.
input w_clk, // Write domain clock
input w_rst, // Write domain reset
input r_en, // Read data and increment addr.
input r_clk, // Read domain clock
input r_rst, // Read domain reset
// Outputs
output w_full, // Flag: 1 if FIFO is full
output reg [DATA_SIZE-1:0] r_data, // Data to be read from FIFO
output r_empty // Flag: 1 if FIFO is empty
);
// Constants
localparam FIFO_DEPTH = (1 << ADDR_SIZE);
// Internal signals
wire [ADDR_SIZE-1:0] w_addr;
wire [ADDR_SIZE:0] w_gray;
wire [ADDR_SIZE-1:0] r_addr;
wire [ADDR_SIZE:0] r_gray;
// Internal storage elements
reg [ADDR_SIZE:0] w_syn_r_gray;
reg [ADDR_SIZE:0] w_syn_r_gray_pipe;
reg [ADDR_SIZE:0] r_syn_w_gray;
reg [ADDR_SIZE:0] r_syn_w_gray_pipe;
// Declare memory
reg [DATA_SIZE-1:0] mem [0:FIFO_DEPTH-1];
//--------------------------------------------------------------------------
// Dual-port memory (should be inferred as block RAM)
// Write data logic for dual-port memory (separate write clock)
// Do not write if FIFO is full!
always @ (posedge w_clk) begin
if (w_en & ~w_full) begin
mem[w_addr] <= w_data;
end
end
// Read data logic for dual-port memory (separate read clock)
// Do not read if FIFO is empty!
always @ (posedge r_clk) begin
if (r_en & ~r_empty) begin
r_data <= mem[r_addr];
end
end
//--------------------------------------------------------------------------
// Synchronizer logic
// Pass read-domain Gray code pointer to write domain
always @ (posedge w_clk or posedge w_rst) begin
if (w_rst == 1'b1) begin
w_syn_r_gray_pipe <= 0;
w_syn_r_gray <= 0;
end else begin
w_syn_r_gray_pipe <= r_gray;
w_syn_r_gray <= w_syn_r_gray_pipe;
end
end
// Pass write-domain Gray code pointer to read domain
always @ (posedge r_clk or posedge r_rst) begin
if (r_rst == 1'b1) begin
r_syn_w_gray_pipe <= 0;
r_syn_w_gray <= 0;
end else begin
r_syn_w_gray_pipe <= w_gray;
r_syn_w_gray <= r_syn_w_gray_pipe;
end
end
//--------------------------------------------------------------------------
// Instantiate incrementer and full/empty checker modules
// Write address increment and full check module
w_ptr_full #(.ADDR_SIZE(ADDR_SIZE)) w_ptr_full (
.w_syn_r_gray(w_syn_r_gray),
.w_inc(w_en),
.w_clk(w_clk),
.w_rst(w_rst),
.w_addr(w_addr),
.w_gray(w_gray),
.w_full(w_full)
);
// Read address increment and empty check module
r_ptr_empty #(.ADDR_SIZE(ADDR_SIZE)) r_ptr_empty (
.r_syn_w_gray(r_syn_w_gray),
.r_inc(r_en),
.r_clk(r_clk),
.r_rst(r_rst),
.r_addr(r_addr),
.r_gray(r_gray),
.r_empty(r_empty)
);
endmodule
module bytegen(input clk,
// input rst,
input clkout,
input txe,
output reg [7:0] outdata,
output reg wrb,
output siwu,
output [7:0] led);
wire zoomieclk;
SB_PLL40_CORE #(
.FEEDBACK_PATH("SIMPLE"),
.DIVR(4'b0000), // DIVR = 0
.DIVF(7'd66),
.DIVQ(3'd5),
.FILTER_RANGE(3'b001) // FILTER_RANGE = 1
) uut (
.LOCK(locked),
.RESETB(1'b1),
.BYPASS(1'b0),
.REFERENCECLK(clk),
.PLLOUTCORE(zoomieclk)
);
reg rst = 1'b0;
assign siwu = 1'b1;
reg [19:0] counter = 20'b0;
reg w_en;
reg r_en;
wire r_empty;
wire [7:0] r_data;
wire w_full;
async_fifo #(.ADDR_SIZE(10)) fifo(.w_data(counter[7:0]),
.w_en(w_en),
.w_clk(zoomieclk),
.w_rst(rst),
.w_full(w_full),
.r_en(r_en),
.r_clk(~clkout),
.r_rst(rst),
.r_data(r_data),
.r_empty(r_empty)
);
reg running = 1'b0;
always @(posedge zoomieclk)
begin
if (~w_full)
begin
w_en <= 1'b1;
running <= 1'b1;
if (running)
counter <= counter + 1;
end
else
begin
running <= 1'b0;
w_en <= 1'b0;
end
end
reg [2:0] state = 0;
parameter ReadData=0, StrobeWRB=2, Finish=3, WaitForTX=1;
reg [4:0] txehigh = 0;
wire tx_throttle;
assign tx_throttle = txehigh>5;
always @(negedge clkout)
begin
// count how long txe has been high
// we have to throttle when it has been
// high for 400ns. we'll try throttling sooner.
if ((txe) & (txehigh < 29))
begin
txehigh <= txehigh + 1;
end
else if(~txe)
begin
txehigh <= 5'b0;
end
casez(state)
ReadData:
begin
outdata <= r_data;
r_en <= 1'b0;
if(txe)
state <= WaitForTX;
else
state <= StrobeWRB;
end // case: ReadData
WaitForTX:
begin
if(~txe)
state <= StrobeWRB;
end
StrobeWRB:
begin
wrb <= 1'b0;
state <= Finish;
end
Finish:
begin
wrb <= 1'b1;
if (~r_empty)
begin
r_en <= 1'b1;
state <= ReadData;
end
end
endcase
end
// stuff that was useful at some point in development
assign led[0] = txe;
assign led[1] = wrb;
assign led[2] = w_full;
assign led[3] = r_empty;
assign led[4] = (state==ReadData);
assign led[5] = (state==WaitForTX);
assign led[6] = (state==StrobeWRB);
assign led[7] = (state==Finish);
/*
assign led[6:4] = 5'b0;
assign led[7] = counter[19];
*/
endmodule // bytegen
// Increment read address and check if FIFO is empty
module r_ptr_empty #(
// Parameters
parameter ADDR_SIZE = 4 // Number of bits for address
) (
// Inputs
input [ADDR_SIZE:0] r_syn_w_gray, // Synced write Gray pointer
input r_inc, // 1 to increment address
input r_clk, // Read domain clock
input r_rst, // Read domain reset
// Outputs
output [ADDR_SIZE-1:0] r_addr, // Mem address to read from
output reg [ADDR_SIZE:0] r_gray, // Gray address with +1 MSb
output reg r_empty // 1 if FIFO is empty
);
// Internal signals
wire [ADDR_SIZE:0] r_gray_next; // Gray code version of address
wire [ADDR_SIZE:0] r_bin_next; // Binary version of address
wire r_empty_val; // FIFO is empty
// Internal storage elements
reg [ADDR_SIZE:0] r_bin; // Registered binary address
// Drop extra most significant bit (MSb) for addressing into memory
assign r_addr = r_bin[ADDR_SIZE-1:0];
// Be ready with next (incremented) address (if inc set and not empty)
assign r_bin_next = r_bin + (r_inc & ~r_empty);
// Convert next binary address to Gray code value
assign r_gray_next = (r_bin_next >> 1) ^ r_bin_next;
// If the synced write Gray code is equal to the current read Gray code,
// then the pointers have caught up to each other and the FIFO is empty
assign r_empty_val = (r_gray_next == r_syn_w_gray);
// Register the binary and Gray code pointers in the read clock domain
always @ (posedge r_clk or posedge r_rst) begin
if (r_rst == 1'b1) begin
r_bin <= 0;
r_gray <= 0;
end else begin
r_bin <= r_bin_next;
r_gray <= r_gray_next;
end
end
// Register the empty flag
always @ (posedge r_clk or posedge r_rst) begin
if (r_rst == 1'b1) begin
r_empty <= 1'b1;
end else begin
r_empty <= r_empty_val;
end
end
endmodule
// started from the libftdi1 example, which was... suspect at best
// this is all targetted at the FTDI 232H. no guarantee for anything else.
// c++ stream_dump.c -o stream_dump `pkg-config --cflags --libs libftdi1`
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <getopt.h>
#include <errno.h>
#include <ftdi.h>
// yeah we're c++ now. i'm too lazy
#include <chrono>
int main(int argc, char **argv)
{
struct ftdi_context *ftdi;
int err;
char *descstring = NULL;
if ((ftdi = ftdi_new()) == 0)
{
fprintf(stderr, "ftdi_new failed\n");
return EXIT_FAILURE;
}
if (ftdi_set_interface(ftdi, INTERFACE_A) < 0)
{
fprintf(stderr, "ftdi_set_interface failed\n");
ftdi_free(ftdi);
return EXIT_FAILURE;
}
if (ftdi_usb_open_desc(ftdi, 0x0403, 0x6014, descstring, NULL) < 0)
{
fprintf(stderr,"Can't open ftdi device: %s\n",ftdi_get_error_string(ftdi));
ftdi_free(ftdi);
return EXIT_FAILURE;
}
if (ftdi_set_bitmode(ftdi, 0xff, 0x00) < 0)
{
fprintf(stderr,"Can't reset fifo mode, Error %s\n",ftdi_get_error_string(ftdi));
ftdi_usb_close(ftdi);
ftdi_free(ftdi);
return EXIT_FAILURE;
}
// i've seen this mentioned but i suspect it isn't useful
sleep(1);
// this is the magic that flips FT245 async mode into sync mode
if (ftdi_set_bitmode(ftdi, 0xff, 0x40) < 0)
{
fprintf(stderr,"Can't set synchronous fifo mode, Error %s\n",ftdi_get_error_string(ftdi));
ftdi_usb_close(ftdi);
ftdi_free(ftdi);
return EXIT_FAILURE;
}
// 1 seems better than 2. 0 isn't an option.
if(ftdi_set_latency_timer(ftdi, 1))
{
fprintf(stderr,"Can't set latency, Error %s\n",ftdi_get_error_string(ftdi));
ftdi_usb_close(ftdi);
ftdi_free(ftdi);
return EXIT_FAILURE;
}
// doesn't seem to have an effect
/*
unsigned int chunksize = 1<<15;
if(ftdi_write_data_get_chunksize(ftdi, &chunksize))
{
fprintf(stderr, "can't set get chunksize error %s\n", ftdi_get_error_string(ftdi));
ftdi_usb_close(ftdi);
ftdi_free(ftdi);
return EXIT_FAILURE;
}
*/
// this is mentioned in some docs, but doesn't seem to have any impact
/*
if(ftdi_setflowctrl(ftdi, SIO_RTS_CTS_HS))
{
fprintf(stderr,"failed to adjust flow control %s\n", ftdi_get_error_string(ftdi));
ftdi_usb_close(ftdi);
ftdi_free(ftdi);
return EXIT_FAILURE;
}
*/
// ftdi_streamread seems to be junk, this is a ftdi_read_data loop
// we're both measuring the rate at which we read data, and checking
// to make sure that we're not dropping bytes.
uint8_t previous = 0;
uint64_t processed = 0;
uint64_t mark = 0;
uint64_t prev_processed = 0;
auto prev_time = std::chrono::high_resolution_clock::now();
auto start = std::chrono::high_resolution_clock::now();
while(1) {
// 1024 at least on my computer did slightly better than 512 and 2048
uint8_t buffer[1024];
int length = ftdi_read_data(ftdi, buffer, 1024);
auto now = std::chrono::high_resolution_clock::now();
if (length < 0) {
fprintf(stderr, "ftdi_read_data error %i %s\n", length, ftdi_get_error_string(ftdi));
exit(1);
}
for(int i=0; i<length; i++) {
if( ((previous+1)%256) != buffer[i] )
printf("mismatch %i %i\n", previous, buffer[i]);
previous = buffer[i];
}
processed += length;
if(processed > (mark * 10 * 1024 * 1024)) {
std::chrono::duration<float> diff = now - start;
std::chrono::duration<float> prevdiff = now - prev_time;
printf("processed %i / %.2f = %.3fMB/s (%.2fMB/s)\n",
processed, diff.count(),
(processed/diff.count())/(1024*1024),
((processed - prev_processed)/(prevdiff.count()))/(1024*1024));
mark += 1;
prev_time = now;
prev_processed = processed;
}
}
// put it back to the original state? sure why not
if (ftdi_set_bitmode(ftdi, 0xff, BITMODE_RESET) < 0)
{
fprintf(stderr,"Can't reset fifo mode, Error %s\n",ftdi_get_error_string(ftdi));
ftdi_usb_close(ftdi);
ftdi_free(ftdi);
return EXIT_FAILURE;
}
ftdi_usb_close(ftdi);
ftdi_free(ftdi);
exit (0);
}
// Increment write address and check if FIFO is full
module w_ptr_full #(
// Parameters
parameter ADDR_SIZE = 4 // Number of bits for address
) (
// Inputs
input [ADDR_SIZE:0] w_syn_r_gray, // Synced read Gray pointer
input w_inc, // 1 to increment address
input w_clk, // Write domain clock
input w_rst, // Write domain reset
// Outputs
output [ADDR_SIZE-1:0] w_addr, // Mem address to write to
output reg [ADDR_SIZE:0] w_gray, // Gray adress with +1 MSb
output reg w_full // 1 if FIFO is full
);
// Internal signals
wire [ADDR_SIZE:0] w_gray_next; // Gray code version of address
wire [ADDR_SIZE:0] w_bin_next; // Binary version of address
wire w_full_val; // FIFO is full
// Internal storage elements
reg [ADDR_SIZE:0] w_bin; // Registered binary address
// Drop extra most significant bit (MSb) for addressing into memory
assign w_addr = w_bin[ADDR_SIZE-1:0];
// Be ready with next (incremented) address (if inc set and not full)
assign w_bin_next = w_bin + (w_inc & ~w_full);
// Convert next binary address to Gray code value
assign w_gray_next = (w_bin_next >> 1) ^ w_bin_next;
// Compare write Gray code to synced read Gray code to see if FIFO is full
// If: extra MSb of read and write Gray codes are not equal AND
// 2nd MSb of read and write Gray codes are not equal AND
// the rest of the bits are equal
// Then: address pointers are same with write pointer ahead by 2^ADDR_SIZE
// elements (i.e. wrapped around), so FIFO is full.
assign w_full_val = ((w_gray_next[ADDR_SIZE] != w_syn_r_gray[ADDR_SIZE]) &&
(w_gray_next[ADDR_SIZE-1] != w_syn_r_gray[ADDR_SIZE-1]) &&
(w_gray_next[ADDR_SIZE-2:0] == w_syn_r_gray[ADDR_SIZE-2:0]));
// Register the binary and Gray code pointers in the write clock domain
always @ (posedge w_clk or posedge w_rst) begin
if (w_rst == 1'b1) begin
w_bin <= 0;
w_gray <= 0;
end else begin
w_bin <= w_bin_next;
w_gray <= w_gray_next;
end
end
// Register the full flag
always @ (posedge w_clk or posedge w_rst) begin
if (w_rst == 1'b1) begin
w_full <= 1'b0;
end else begin
w_full <= w_full_val;
end
end
endmodule
@jkominek
Copy link
Author

Oh, part of the secret sauce here is that the FIFO needs to be 2^9 words in size. 2^8 words caused it to back up. Utilization report, fwiw:

Info: Device utilisation:
Info:            ICESTORM_LC:   177/ 7680     2%
Info:           ICESTORM_RAM:     2/   32     6%
Info:                  SB_IO:    21/  256     8%
Info:                  SB_GB:     2/    8    25%
Info:           ICESTORM_PLL:     1/    2    50%
Info:            SB_WARMBOOT:     0/    1     0%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment