Last active
June 15, 2024 04:40
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 |
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
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.