Browse Source

cores/ice40: Add the ice40 4x SERDES cores

Signed-off-by: Sylvain Munaut <tnt@246tNt.com>
Sylvain Munaut 4 năm trước cách đây
mục cha
commit
1827bf2ea3

+ 5 - 0
cores/ice40/core.mk

@@ -3,6 +3,11 @@ CORE := ice40
 RTL_SRCS_ice40 := $(addprefix rtl/, \
 	ice40_ebr.v \
 	ice40_spram_gen.v \
+	ice40_iserdes.v \
+	ice40_oserdes.v \
+	ice40_serdes_crg.v \
+	ice40_serdes_dff.v \
+	ice40_serdes_sync.v \
 )
 
 TESTBENCHES_ice40 := \

+ 302 - 0
cores/ice40/rtl/ice40_iserdes.v

@@ -0,0 +1,302 @@
+/*
+ * ice40_iserdes.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * BSD 3-clause, see LICENSE.bsd
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the <organization> nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+`default_nettype none
+
+module ice40_iserdes #(
+	parameter EDGE_SEL  = "SINGLE_POS",	// "SINGLE_POS" / "SINGLE_NEG" / "DUAL_POS" / "DUAL_POS_NEG"
+	parameter PHASE_SEL = "STATIC",		// "STATIC" / "DYNAMIC"
+	parameter integer PHASE = 0,
+	parameter integer SERDES_GRP = 0
+)(
+	input  wire [1:0] d,
+	output wire [3:0] q,
+	input  wire       edge_sel,
+	input  wire [1:0] phase_sel,
+	input  wire       sync,
+	input  wire       clk_1x,
+	input  wire       clk_4x
+);
+
+	genvar i, j;
+
+	/* 				 0	 1
+	 * SINGLE_POS	POS	 /
+	 * SINGLE_NEG	NEG  /
+	 * DUAL_POS		POS  /
+	 * DUAL_POS_NEG	POS	NEG
+	 */
+
+	// FIXME: The DUAL_POS_NEG mode would need a negative edge sync signal as
+	// well
+
+
+	// Signals
+	// -------
+
+	wire [3:0] shift_in[0:1];
+	wire [3:0] shift_out[0:1];
+
+	wire [3:0] fcap_in[0:1];
+	wire [3:0] fcap_out[0:1];
+
+
+	// Fast paths
+	// ----------
+
+	// - For the "SINGLE_{POS,NEG}", we only have a single path
+	// - For the "DUAL_POS_POS" case it's a single path as well with a pre-sel
+	//   mux. If dynamic phase is also enabled, this option will have an added
+	//   delay (because need for mux between path and between phase)
+	// - For the "DUAL_POS_NEG" case, we have two independent paths
+
+	generate
+		for (j=0; j<2; j=j+1) begin
+			if ((j == 0) || (EDGE_SEL == "DUAL_POS_NEG"))
+			begin : fp
+				localparam IS_NEG = (EDGE_SEL == "SINGLE_NEG") || (j == 1);
+				wire edge_active;
+				wire din_mux;
+				wire din;
+
+				// Edge Select
+				// -----------
+
+				assign edge_active = (EDGE_SEL != "DUAL_POS_NEG") || (edge_sel == j);
+
+				if (EDGE_SEL == "DUAL_POS_POS") begin
+					// Need a pre-mux
+					(* dont_touch *)
+					SB_LUT4 #(
+						.LUT_INIT(16'hFC30)
+					) lut_edgemux_I (
+						.I0(1'b0),
+						.I1(edge_sel),
+						.I2(d[0]),
+						.I3(d[1]),	// Fast Path for the neg-edge
+						.O(din_mux)
+					);
+
+					if (PHASE_SEL == "DYNAMIC")
+						// If we have dynamic phase, we need the added stage
+						// for timing
+						ice40_serdes_dff #(
+							.NEG(IS_NEG),
+							.SERDES_GRP( (SERDES_GRP << 8) | 'h4b0 | (j << 4) )
+						) dff_edgemux_I (
+							.d(din_mux),
+							.q(din),
+							.c(clk_4x)
+						);
+					else
+						// This mux can be packed with the first shift
+						// register stage
+						assign din = din_mux;
+
+				end else begin
+					// Directly from IOB signal
+					assign din = d[j];
+				end
+
+
+				// Shifter
+				// -------
+
+				assign shift_in[j] = { shift_out[j][2:0], din };
+
+				for (i=0; i<4; i=i+1)
+				begin
+					ice40_serdes_dff #(
+						.NEG(IS_NEG),
+						.SERDES_GRP( (SERDES_GRP << 8) | 'h4a0 | (j << 4) | i )
+					) dff_shift_I (
+						.d(shift_in[j][i]),
+						.q(shift_out[j][i]),
+						.c(clk_4x)
+					);
+				end
+
+
+				// Fast Capture
+				// ------------
+
+				// If we have dynamic phase selection, apply the LSB here
+				if (PHASE_SEL == "DYNAMIC")
+					assign fcap_in[j] = edge_active ? (phase_sel[0] ? shift_out[j] : shift_in[j]) : 4'h0;
+				else
+					assign fcap_in[j] = edge_active ? shift_out[j] : 4'h0;
+
+				// Register
+				for (i=0; i<4; i=i+1)
+				begin
+					ice40_serdes_dff #(
+						.NEG(IS_NEG),
+						.ENA(1),
+						.SERDES_GRP( (SERDES_GRP << 8) | 'h490 | (j << 4) | i )
+					) dff_shift_I (
+						.d(fcap_in[j][i]),
+						.q(fcap_out[j][i]),
+						.e(sync),
+						.c(clk_4x)
+					);
+				end
+			end
+			else
+			begin
+				// Dummy
+				assign fcap_out[j]  = 4'h0;
+			end
+		end
+	endgenerate
+
+
+	// Slow Capture
+	// ------------
+
+	generate
+		if (PHASE_SEL == "STATIC")
+		begin
+			// Static Phase
+			// - - - - - - -
+
+			wire [3+PHASE:0] scap_in;
+			wire [3+PHASE:0] scap_out;
+
+			// Input
+			if (PHASE > 0)
+				assign scap_in[3+PHASE:4] = scap_out[PHASE-1:0];
+
+			assign scap_in[3:0] = fcap_out[0] | fcap_out[1];
+
+			// Registers
+			for (i=0; i<(4+PHASE); i=i+1)
+				ice40_serdes_dff #(
+					.SERDES_GRP( (SERDES_GRP << 8) | 'h680 | i )
+				) dff_scap_I (
+					.d(scap_in[i]),
+					.q(scap_out[i]),
+					.c(clk_1x)
+				);
+
+			// Output
+			assign q = scap_out[3+PHASE:PHASE];
+		end
+		else
+		begin
+			// Dynamic Phase
+			// - - - - - - -
+
+			wire [5:0] scap_in;
+			wire [5:0] scap_out;
+
+			// Input
+			if (EDGE_SEL == "DUAL_POS_NEG")
+			begin
+
+				// Dual Edge Path
+				// - - - - - - - -
+
+				wire [1:0] scap_pre_or;
+
+				// Pre-OR
+				(* SERDES_GRP=( (SERDES_GRP << 8) | 'h680 | 6 ) *)
+				(* dont_touch *)
+				SB_LUT4 #(
+					.LUT_INIT(16'hFFF0)
+				) or_lut_2_I (
+					.I0(1'b0),
+					.I1(1'b0),
+					.I2(fcap_out[1][2]),
+					.I3(fcap_out[0][2]),
+					.O(scap_pre_or[0])
+				);
+
+				(* SERDES_GRP=( (SERDES_GRP << 8) | 'h680 | 7 ) *)
+				(* dont_touch *)
+				SB_LUT4 #(
+					.LUT_INIT(16'hFFF0)
+				) or_lut_3_I (
+					.I0(1'b0),
+					.I1(1'b0),
+					.I2(fcap_out[1][3]),
+					.I3(fcap_out[0][3]),
+					.O(scap_pre_or[1])
+				);
+
+				// Main muxes
+				(* dont_touch *)
+				SB_LUT4 #(
+					.LUT_INIT(16'hFE54)
+				) mux_lut_I[3:0] (
+					.I0(phase_sel[1]),
+					.I1(fcap_out[1][3:0]),
+					.I2(fcap_out[0][3:0]),
+					.I3({scap_out[5:4], scap_pre_or}),
+					.O(scap_in[3:0])
+				);
+
+				// Save regs
+				assign scap_in[5:4] = fcap_out[0][1:0] | fcap_out[1][1:0];
+
+			end
+			else
+			begin
+
+				// Single Edge Path
+				// - - - - - - - - -
+
+				assign scap_in = {
+					fcap_out[0][1:0],
+					phase_sel[1] ?
+						{ scap_out[5:4], fcap_out[0][3:2] } :
+						fcap_out[0][3:0]
+				};
+
+			end
+
+			// Registers
+			for (i=0; i<6; i=i+1)
+				ice40_serdes_dff #(
+					.SERDES_GRP( (SERDES_GRP << 8) | 'h680 | i )
+				) dff_scap_I (
+					.d(scap_in[i]),
+					.q(scap_out[i]),
+					.c(clk_1x)
+				);
+
+			// Output
+			assign q = scap_out[3:0];
+		end
+	endgenerate
+
+endmodule

+ 120 - 0
cores/ice40/rtl/ice40_oserdes.v

@@ -0,0 +1,120 @@
+/*
+ * ice40_oserdes.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * BSD 3-clause, see LICENSE.bsd
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the <organization> nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+`default_nettype none
+
+module ice40_oserdes #(
+	parameter MODE = "DATA",	// "DATA" / "CLK90_2X" / "CLK90_4X"
+	parameter integer SERDES_GRP = 0
+)(
+	input  wire [3:0] d,
+	output wire [1:0] q,
+	input  wire       sync,
+	input  wire       clk_1x,
+	input  wire       clk_4x
+);
+
+	genvar i;
+
+
+	// Signals
+	// -------
+
+	wire [3:0] cap_in;
+	wire [3:0] cap_out;
+
+	wire [3:0] shift_in;
+	wire [3:0] shift_out;
+
+	wire       delay_out;
+
+
+	// Capture
+	// -------
+
+	assign cap_in = (MODE == "CLK90_2X") ? { d[1], 1'b0, d[0], 1'b0 } : d;
+
+	generate
+		for (i=0; i<4; i=i+1)
+			ice40_serdes_dff #(
+				.SERDES_GRP( (SERDES_GRP << 8) | 'h00 | i )
+			) dff_cap_I (
+				.d(cap_in[i]),
+				.q(cap_out[i]),
+				.c(clk_1x)
+			);
+	endgenerate
+
+
+	// Shifter
+	// -------
+
+	assign shift_in = sync ? cap_out : { shift_out[2:0], 1'b0 };
+
+	generate
+		for (i=0; i<4; i=i+1)
+			ice40_serdes_dff #(
+				.SERDES_GRP( (SERDES_GRP << 8) | 'h10 | i )
+			) dff_shift_I (
+				.d(shift_in[i]),
+				.q(shift_out[i]),
+				.c(clk_4x)
+			);
+	endgenerate
+
+
+	// Output
+	// ------
+
+	generate
+		if ((MODE == "CLK90_2X") || (MODE == "CLK90_4X")) begin
+			// Delay FF for falling edge
+			ice40_serdes_dff #(
+				.SERDES_GRP( (SERDES_GRP << 8) | 'h20 )
+			) dff_out_I (
+				.d(shift_out[3]),
+				.q(delay_out),
+				.c(clk_4x)
+			);
+
+			// Output depends on clock mode
+			assign q[0] = (MODE == "CLK90_2X") ? delay_out : 1'b0;
+			assign q[1] = delay_out;
+		end else begin
+			// Simple data map, fall edge output un-used
+			assign q[0] = shift_out[3];
+			assign q[1] = 1'b0;
+		end
+	endgenerate
+
+endmodule

+ 121 - 0
cores/ice40/rtl/ice40_serdes_crg.v

@@ -0,0 +1,121 @@
+/*
+ * ice40_serdes_crg.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * BSD 3-clause, see LICENSE.bsd
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the <organization> nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+`default_nettype none
+
+module ice40_serdes_crg #(
+	parameter integer NO_CLOCK_2X = 0
+)(
+	// Input from PLL
+	input  wire clk_4x,
+	input  wire pll_lock,
+
+	// Outputs
+	output wire clk_1x,
+	output wire clk_2x,
+	output wire rst
+);
+
+	// Signals
+	// -------
+
+	// Reset
+	reg  [3:0] rst_cnt_nxt[0:15];
+	reg  [3:0] rst_cnt = 4'h8;
+	reg        rst_i;
+
+	// Clock Divider
+	reg  [1:0] clk_div;
+	wire       clk_sync_i;
+
+
+	// Reset
+	// -----
+
+	// Counter
+	initial begin : rst_init
+		integer i;
+		for (i=0; i<16; i=i+1)
+			rst_cnt_nxt[i] = i==15 ? i : (i+1);
+	end
+
+	always @(posedge clk_4x or negedge pll_lock)
+		if (~pll_lock)
+			rst_cnt <= 4'h0;
+		else
+			rst_cnt <= rst_cnt_nxt[rst_cnt];
+
+	// Final FF
+	always @(posedge clk_4x or negedge pll_lock)
+		if (~pll_lock)
+			rst_i <= 1'b1;
+		else
+			rst_i <= (rst_cnt != 4'hf);
+
+	// Buffer reset
+	SB_GB gbuf_rst_I (
+		.USER_SIGNAL_TO_GLOBAL_BUFFER(rst_i),
+		.GLOBAL_BUFFER_OUTPUT(rst)
+	);
+
+
+	// Clock Divider & Sync
+	// --------------------
+
+	// Simple counter to generate the edges
+	always @(posedge clk_4x or negedge pll_lock)
+		if (~pll_lock)
+			clk_div <= 2'b00;
+		else
+			clk_div <= clk_div + rst_cnt[3];
+
+	// Buffer clk_2x
+	generate
+		if (NO_CLOCK_2X)
+			assign clk_2x = 1'b0;
+		else
+			(* BEL="X13/Y0/gb" *)
+			SB_GB gbuf_2x_I (
+				.USER_SIGNAL_TO_GLOBAL_BUFFER(clk_div[0]),
+				.GLOBAL_BUFFER_OUTPUT(clk_2x)
+			);
+	endgenerate
+
+	// Buffer clk_1x
+	(* BEL="X12/Y0/gb" *)
+	SB_GB gbuf_1x_I (
+		.USER_SIGNAL_TO_GLOBAL_BUFFER(clk_div[1]),
+		.GLOBAL_BUFFER_OUTPUT(clk_1x)
+	);
+
+endmodule

+ 134 - 0
cores/ice40/rtl/ice40_serdes_dff.v

@@ -0,0 +1,134 @@
+/*
+ * ice40_serdes_dff.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * BSD 3-clause, see LICENSE.bsd
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the <organization> nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+`default_nettype none
+
+module ice40_serdes_dff #(
+	parameter integer NEG = 0,
+	parameter integer ENA = 0,
+	parameter integer RST = 0,
+	parameter integer SERDES_GRP = -1,
+	parameter BEL = ""
+)(
+	input  wire d,
+	output wire q,
+	input  wire e,
+	input  wire r,
+	input  wire c
+);
+	parameter TYPE = (RST ? 4 : 0) | (ENA ? 2 : 0) | (NEG ? 1 : 0);
+
+	generate
+		if (TYPE == 0)			// Simple
+			(* BEL=BEL, SERDES_GRP=SERDES_GRP *)
+			(* dont_touch *)
+			SB_DFF dff_I (
+				.D(d),
+				.Q(q),
+				.C(c)
+			);
+
+		else if (TYPE == 1)		// NEG
+			(* BEL=BEL, SERDES_GRP=SERDES_GRP *)
+			(* dont_touch *)
+			SB_DFFN dff_I (
+				.D(d),
+				.Q(q),
+				.C(c)
+			);
+
+		else if (TYPE == 2)		//     ENA
+			(* BEL=BEL, SERDES_GRP=SERDES_GRP *)
+			(* dont_touch *)
+			SB_DFFE dff_I (
+				.D(d),
+				.Q(q),
+				.E(e),
+				.C(c)
+			);
+
+		else if (TYPE == 3)		// NEG ENA
+			(* BEL=BEL, SERDES_GRP=SERDES_GRP *)
+			(* dont_touch *)
+			SB_DFFNE dff_I (
+				.D(d),
+				.Q(q),
+				.E(e),
+				.C(c)
+			);
+
+		else if (TYPE == 4)		//         RST
+			(* BEL=BEL, SERDES_GRP=SERDES_GRP *)
+			(* dont_touch *)
+			SB_DFFR dff_I (
+				.D(d),
+				.Q(q),
+				.R(r),
+				.C(c)
+			);
+
+		else if (TYPE == 5)		// NEG     RST
+			(* BEL=BEL, SERDES_GRP=SERDES_GRP *)
+			(* dont_touch *)
+			SB_DFFNR dff_I (
+				.D(d),
+				.Q(q),
+				.R(r),
+				.C(c)
+			);
+
+		else if (TYPE == 6)		//     ENA RST
+			(* BEL=BEL, SERDES_GRP=SERDES_GRP *)
+			(* dont_touch *)
+			SB_DFFER dff_I (
+				.D(d),
+				.Q(q),
+				.E(e),
+				.R(r),
+				.C(c)
+			);
+
+		else if (TYPE == 7)		// NEG ENA RST
+			(* BEL=BEL, SERDES_GRP=SERDES_GRP *)
+			(* dont_touch *)
+			SB_DFFNER dff_I (
+				.D(d),
+				.Q(q),
+				.E(e),
+				.R(r),
+				.C(c)
+			);
+
+	endgenerate
+
+endmodule

+ 157 - 0
cores/ice40/rtl/ice40_serdes_sync.v

@@ -0,0 +1,157 @@
+/*
+ * ice40_serdes_sync.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * BSD 3-clause, see LICENSE.bsd
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the <organization> nor the
+ *       names of its contributors may be used to endorse or promote products
+ *       derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+`default_nettype none
+
+module ice40_serdes_sync #(
+	parameter integer PHASE = 0,
+	parameter integer NEG_EDGE = 0,
+	parameter integer GLOBAL_BUF = 0,
+	parameter BEL_BASE = "X12/Y15"
+)(
+	input  wire clk_slow,
+	input  wire clk_fast,
+	input  wire rst,
+	output wire sync
+);
+
+	wire [1:0] clk_samp;
+	wire [1:0] edge_det;
+	wire [1:0] edge_found;
+	wire [1:0] cnt_next;
+	wire [1:0] cnt_val;
+
+	wire       sync_next;
+	wire       sync_i;
+
+	// Double sample of the slow clock
+	ice40_serdes_dff #(
+		.NEG(NEG_EDGE),
+		.RST(1),
+		.BEL({BEL_BASE, "/lc0"})
+	) ff_samp0_I (
+		.d(clk_slow),
+		.q(clk_samp[0]),
+		.c(clk_fast),
+		.r(rst)
+	);
+
+	ice40_serdes_dff #(
+		.NEG(NEG_EDGE),
+		.RST(1),
+		.BEL({BEL_BASE, "/lc1"})
+	) ff_samp1_I (
+		.d(clk_samp[0]),
+		.q(clk_samp[1]),
+		.c(clk_fast),
+		.r(rst)
+	);
+
+	// Detect falling edge, then rising edge
+	assign edge_det[0] = edge_found[0] | (clk_samp[1] & ~clk_samp[0]);
+	assign edge_det[1] = edge_found[1] | (clk_samp[0] & ~clk_samp[1] & edge_found[0]);
+
+	ice40_serdes_dff #(
+		.NEG(NEG_EDGE),
+		.RST(1),
+		.BEL({BEL_BASE, "/lc2"})
+	) ff_edge0_I (
+		.d(edge_det[0]),
+		.q(edge_found[0]),
+		.c(clk_fast),
+		.r(rst)
+	);
+
+	ice40_serdes_dff #(
+		.NEG(NEG_EDGE),
+		.RST(1),
+		.BEL({BEL_BASE, "/lc3"})
+	) ff_edge1_I (
+		.d(edge_det[1]),
+		.q(edge_found[1]),
+		.c(clk_fast),
+		.r(rst)
+	);
+
+	// 2 bit upcounter
+	assign cnt_next[0] = cnt_val[0] ^ edge_found[1];
+	assign cnt_next[1] = cnt_val[1] ^ (cnt_val[0] & edge_found[1]);
+
+	ice40_serdes_dff #(
+		.NEG(NEG_EDGE),
+		.RST(1),
+		.BEL({BEL_BASE, "/lc4"})
+	) ff_cnt0_I (
+		.d(cnt_next[0]),
+		.q(cnt_val[0]),
+		.c(clk_fast),
+		.r(rst)
+	);
+
+	ice40_serdes_dff #(
+		.NEG(NEG_EDGE),
+		.RST(1),
+		.BEL({BEL_BASE, "/lc5"})
+	) ff_cnt1_I (
+		.d(cnt_next[1]),
+		.q(cnt_val[1]),
+		.c(clk_fast),
+		.r(rst)
+	);
+
+	// Final comparator
+	assign sync_next = edge_found[1] & (cnt_val == PHASE);
+
+	ice40_serdes_dff #(
+		.NEG(NEG_EDGE),
+		.RST(1),
+		.BEL({BEL_BASE, "/lc6"})
+	) ff_sync_I (
+		.d(sync_next),
+		.q(sync_i),
+		.c(clk_fast),
+		.r(rst)
+	);
+
+	// Buffer ?
+	generate
+		if (GLOBAL_BUF)
+			SB_GB gbuf_sync_I (
+				.USER_SIGNAL_TO_GLOBAL_BUFFER(sync_i),
+				.GLOBAL_BUFFER_OUTPUT(sync)
+			);
+		else
+			assign sync = sync_i;
+	endgenerate
+
+endmodule

+ 401 - 0
cores/ice40/sw/serdes-nextpnr-place.py

@@ -0,0 +1,401 @@
+#!/usr/bin/env python3
+
+from collections import namedtuple
+import re
+
+
+# SerDes group numbers:
+#  [15:12] Group
+#  [11: 8] SubGroup
+#  [ 7: 4] Type
+#  [    3] n/a
+#  [ 2: 0] LC number
+#
+# SubGroups:
+#
+#  0 Data Out path 0
+#  1 Data Out path 1
+#  2 Data Out Enable
+#
+#  4 Data In  path 0
+#  5 Data In  path 1
+#  6 Data In  common
+#
+#
+# Types:
+#
+# 0 OSERDES Capture
+# 1 OSERDES Shift
+# 2 OSERDES NegEdge Delay
+#
+# 8 ISERDES Slow Capture
+# 9 ISERDES Fast Capture
+# a ISERDES Shift
+# b ISERDES PreMux
+#
+#
+# Placement priority
+#
+#        type    near
+#        2       io      Output Neg Edge delay
+#        b       io      Input Pre mux
+#        a       io      Input Shift
+#        9       'a'     Input Fast Capture
+#        1       io      Output Shift
+#        0       '1'     Output Capture
+#        8       '9's    Input Slow Capture
+#
+
+
+class BEL(namedtuple('BEL', 'x y z')):
+
+	@classmethod
+	def from_json_attr(kls, v):
+		def to_int(s):
+			return int(re.sub(r'[^\d-]+', '', s))
+		return kls(*[to_int(x) for x in v.split('/', 3)])
+
+
+class ControlGroup(namedtuple('ControlGroup', 'clk rst ena neg')):
+
+	@classmethod
+	def from_lc(kls, lc):
+		netname = lambda lc, p: lc.ports[p].net.name if (lc.ports[p].net is not None) else None
+		return kls(
+			netname(lc, 'CLK'),
+			netname(lc, 'SR'),
+			netname(lc, 'CEN'),
+			lc.params['NEG_CLK'] == '1'
+		)
+
+
+class FullCellId(namedtuple('SDGId', 'gid sid typ lc')):
+
+	@classmethod
+	def from_json_attr(kls, v):
+		return kls(
+			v >> 12,
+			(v >> 8) & 0xf,
+			(v >> 4) & 0xf,
+			(v >> 0) & 0x7
+		)
+
+
+class SerDesGroup:
+
+	def __init__(self, gid):
+		self.gid = gid
+		self.blocks = {}
+		self.io = None
+
+	def add_lc(self, lc, fcid=None):
+		# Get Full Cell ID if not provided
+		if fcid is None:
+			grp = int(lc.attrs['SERDES_GRP'], 2)
+			fcid = FullCellId.from_json_attr(grp)
+
+		# Add to the cell list
+		if (fcid.sid, fcid.typ) not in self.blocks:
+			self.blocks[(fcid.sid, fcid.typ)] = SerDesBlock(self, fcid.sid, fcid.typ)
+
+		self.blocks[(fcid.sid, fcid.typ)].add_lc(lc, fcid=fcid)
+
+	def analyze(self):
+		# Process all blocks
+		for blk in self.blocks.values():
+			# Analyze
+			blk.analyze()
+
+			# Check IO
+			if blk.io is not None:
+				if (self.io is not None) and (self.io != blk.io):
+					raise RuntimeError(f'Incompatible IO sites found in SerDes group {self.gid}: {self.io} vs {blk.io}')
+				self.io = blk.io
+
+
+class SerDesBlock:
+
+	NAMES = {
+		0x0: 'OSERDES Capture',
+		0x1: 'OSERDES Shift',
+		0x2: 'OSERDES NegEdge Delay',
+		0x8: 'ISERDES Slow Capture',
+		0x9: 'ISERDES Fast Capture',
+		0xa: 'ISERDES Shift',
+		0xb: 'ISERDES PreMux',
+	}
+
+	def __init__(self, group, sid, typ):
+		# Identity
+		self.group = group
+		self.sid = sid
+		self.typ = typ
+
+		# Container
+		self.lcs = 8 * [None]
+		self.io = None
+		self.cg = None
+
+	def __str__(self):
+		return f'SerDesBlock({self.sid:x}/{self.typ:x} {self.NAMES[self.typ]})'
+
+	def _find_io_site_for_lc(self, lc):
+		# Check in/out ports
+		for pn in [ 'I0', 'I1', 'I2', 'I3', 'O' ]:
+			n = lc.ports[pn].net
+			if (n is None) or n.name.startswith('$PACKER_'):
+				continue
+			pl = [ n.driver ] + list(n.users)
+			for p in pl:
+				if (p.cell.type == 'SB_IO') and ('BEL' in p.cell.attrs):
+					return BEL.from_json_attr(p.cell.attrs['BEL'])
+		return None
+
+	def add_lc(self, lc, fcid=None):
+		# Get Full Cell ID if not provided
+		if fcid is None:
+			grp = int(lc.attrs['SERDES_GRP'], 2)
+			fcid = FullCellId.from_json_attr(grp)
+
+		# Add to LCs
+		if self.lcs[fcid.lc] is not None:
+			raise RuntimeError(f'Duplicate LC for FullCellId {fcid}')
+
+		self.lcs[fcid.lc] = lc
+
+	def find_io_site(self):
+		for lc in self.lcs:
+			if lc is None:
+				continue
+			s = self._find_io_site_for_lc(lc)
+			if s is not None:
+				return s
+		return None
+
+	def analyze(self):
+		# Check and truncate LC array
+		l = len(self)
+		if not all([x is not None for x in self.lcs[0:l]]):
+			raise RuntimeError(f'Invalid group in block {self.group.gid}/{self.sid}/{self.typ}')
+
+		self.lcs = self.lcs[0:l]
+
+		# Identify IO site connection if there is one
+		self.io = self.find_io_site()
+
+		# Identify the control group
+		self.cg = ControlGroup.from_lc(self.lcs[0])
+
+	def assign_bel(self, base_bel, zofs=0):
+		for i, lc in enumerate(self.lcs):
+			lc.setAttr('BEL', 'X%d/Y%d/lc%d' % (base_bel.x, base_bel.y, base_bel.z + zofs + i))
+
+	def __len__(self):
+		return sum([x is not None for x in self.lcs])
+
+
+class PlacerSite:
+
+	def __init__(self, pos):
+		self.pos = pos
+		self.free = 8
+		self.blocks = []
+		self.cg = None
+
+	def valid_for_block(self, blk):
+		return (self.free >= len(blk)) and (
+			(self.cg is None) or
+			(blk.cg is None) or
+			(self.cg == blk.cg)
+		)
+
+	def add_block(self, blk):
+		# Assign the block into position
+		pos = BEL(self.pos.x, self.pos.y, 8-self.free)
+		blk.assign_bel(pos)
+
+		# Add to blocks here
+		self.blocks.append(blk)
+
+		# Update constrainsts
+		self.cg = blk.cg
+		self.free -= len(blk)
+
+		return pos
+
+
+class Placer:
+
+	PRIORITY = [
+		# Type	Place Target
+		(0x2,	lambda p, b: b.group.io),
+		(0xb,	lambda p, b: b.group.io),
+		(0xa,	lambda p, b: b.group.io),
+		(0x9,	lambda p, b: p.pos_of( b.group.blocks[(4|(b.sid & 1), 0xa)] ) ),
+		(0x1,	lambda p, b: b.group.io),
+		(0x0,	lambda p, b: p.pos_of( b.group.blocks[(b.sid, 0x1)]  ) ),
+		(0x8,	lambda p, b: p.pos_of( b.group.blocks[(4, 0xa)], b.group.blocks.get((5, 0xa)) ) ),
+	]
+
+	PLACE_PREF = [
+		# Xofs Yofs
+		( 0,  1),
+		(-1,  1),
+		( 1,  1),
+		(-1,  0),
+		( 1,  0),
+		( 0, -1),
+		(-1, -1),
+		( 1, -1),
+		( 0,  1),
+		( 0,  2),
+		( 0,  3),
+		( 0,  4),
+		(-1,  1),
+		( 1,  1),
+		(-1,  2),
+		( 1,  2),
+		(-1,  3),
+		( 1,  3),
+		(-1,  4),
+		( 1,  4),
+	]
+
+	def __init__(self, groups, top=False):
+		# Save groups to place
+		self.groups = groups
+
+		# Generate site grid
+		self.top = top
+		self.m_fwd  = {}
+		self.m_back = {}
+
+		for y in (range(26,31) if self.top else range(1,6)):
+			for x in range(1,25):
+				# Invalid, used by SPRAM
+				if x in [6,19]:
+					continue
+				self.m_fwd[BEL(x,y,0)] = PlacerSite(BEL(x,y, 0))
+
+	def _blocks_by_type(self, typ):
+		r = []
+		for grp in self.groups:
+			for blk in grp.blocks.values():
+				if blk.typ == typ:
+					r.append(blk)
+		return sorted(r, key=lambda b: (b.group.gid, b.sid))
+
+	def place(self):
+		# Scan by priority order
+		for typ, fn in self.PRIORITY:
+			# Collect all blocks per type and sorted by gid,sid
+			blocks = self._blocks_by_type(typ)
+
+			# Place each block
+			for blk in blocks:
+				# Get target location
+				tgt = fn(self, blk)
+
+				if type(tgt) == list:
+					x = int(round(sum([b.x for b in tgt]) / len(tgt)))
+					y = int(round(sum([b.y for b in tgt]) / len(tgt)))
+					tgt = BEL(x, y, 0)
+
+				# Scan placement preference and try to place
+				for xofs, yofs in self.PLACE_PREF:
+					# Flip for top
+					if self.top:
+						yofs = -yofs
+
+					p = BEL(tgt.x + xofs, tgt.y + yofs, 0)
+
+					if (p in self.m_fwd) and (self.m_fwd[p].valid_for_block(blk)):
+						self.place_block(blk, p)
+						break
+
+				else:
+					raise RuntimeError(f'Unable to place {blk}')
+
+		# Debug
+		if debug:
+			for g in self.groups:
+				print(f"Group {g.gid} for IO {g.io}")
+				for b in g.blocks.values():
+					print(f"\t{str(b):40s}: {len(b)} LCs placed @ {self.pos_of(b)}")
+				print()
+
+	def place_block(self, blk, pos):
+		self.m_back[blk] = self.m_fwd[pos].add_block(blk)
+
+	def pos_of(self, *blocks):
+		if len(blocks) > 1:
+			return [ self.m_back.get(b) for b in blocks if b is not None ]
+		else:
+			return self.m_back.get(blocks[0])
+
+
+
+# ----------------------------------------------------------------------------
+# Main
+# ----------------------------------------------------------------------------
+
+debug = True
+
+
+# Collect
+# -------
+
+groups = {}
+
+for n,c in ctx.cells:
+	# Filter out dummy 'BEL' attributes
+	if 'BEL' in c.attrs:
+		if not c.attrs['BEL'].strip():
+			c.unsetAttr('BEL')
+
+	# Does the cell need grouping ?
+	if 'SERDES_GRP' in c.attrs:
+		# Get group
+		grp = int(c.attrs['SERDES_GRP'], 2)
+		c.unsetAttr('SERDES_GRP')
+
+		# Skip invalid/dummy
+		if grp == 0xffffffff:
+			continue
+
+		# Add LC to our list
+		fcid = FullCellId.from_json_attr(grp)
+
+		if fcid.gid not in groups:
+			groups[fcid.gid] = SerDesGroup(fcid.gid)
+
+		groups[fcid.gid].add_lc(c, fcid=fcid)
+
+
+# Analyze and split into top/bottom
+# ---------------------------------
+
+groups_top = []
+groups_bot = []
+
+for g in groups.values():
+	# Analyze
+	g.analyze()
+
+	# Sort
+	if g.io.y == 0:
+		groups_bot.append(g)
+	else:
+		groups_top.append(g)
+
+
+# Execute placer
+# --------------
+
+if groups_top:
+	p_top = Placer(groups_top, top=True)
+	p_top.place()
+
+if groups_bot:
+	p_bot = Placer(groups_bot, top=False)
+	p_bot.place()