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

Revision of https://gist.github.com/jkominek/a812b2b3c37d7ded47e8143c54de4c1f which adds the Sunburst Design asynchronous FIFO. You can fill that up at whatever rate you want from your clock domain, and then let the FT232H read out from it in synchronous mode, controlled by its 60MHz clock. FYI, it appears that clock stops when you're not reading from the port, so you probably don't want that clock running the bulk of your chip. (Maybe I'm wrong, I didn't specifically investigate that, just seemed to be the case.)

I get sustained transfer rates of about 17.9MB/s, with peaks of 18.42MB/s. That's below the max possible, and People On The Internet report getting the advertised 40MB/s (some of them claim to get faster rates!). If you can manage that and care to share your code, I'd appreciate it. This is already much faster than my application requires, so I am done now.

Since there's a part-specific PLL in there now, I'll note this was all tested on an iCE40HX8k, using Yosys for synthesis. The PLL is only used to boost up the writing side of the FIFO to 25MHz so it can generate data faster than it can be shoveled into the FT232H.

@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