Forráskód Böngészése

cores/hyperram: Initial import of HyperRAM controller

Signed-off-by: Sylvain Munaut <tnt@246tNt.com>
Sylvain Munaut 5 éve
szülő
commit
5554f72a7d

+ 3 - 0
cores/hyperram/Makefile

@@ -0,0 +1,3 @@
+CORE := hyperram
+
+include ../../build/core-rules.mk

+ 13 - 0
cores/hyperram/core.mk

@@ -0,0 +1,13 @@
+CORE := hyperram
+
+RTL_SRCS_hyperram := $(addprefix rtl/, \
+	hram_dline.v \
+	hram_phy_ice40.v \
+	hram_top.v \
+)
+
+TESTBENCHES_hyperram := \
+	hram_top_tb \
+	$(NULL)
+
+include $(ROOT)/build/core-magic.mk

+ 121 - 0
cores/hyperram/doc/hram_regs.md

@@ -0,0 +1,121 @@
+HyperRAM controller Registers
+=============================
+
+
+FIXME: Document the manual command submission for link training
+
+
+Control
+-------
+
+### Control / Status (Read / Write addr `0x00`)
+
+```text
+,-----------------------------------------------------------------------------------------------,
+|31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0|
+|-----------------------------------------------------------------------------------------------|
+|               /             |phase| phy_delay |  cap_lat  |  cmd_lat  |     /     |ir|ic|rs|ru|
+'-----------------------------------------------------------------------------------------------'
+
+ * [21:20] - phy_phase : PHY config - Phase select
+ * [19:16] - phy_delay : PHY config - Delay select
+ * [15:12] - cap_lat   : Capture latency
+ * [11: 8] - cmd_lat   : Command latency
+ * [    3] - ir        : Idle 'run' mode
+ * [    2] - ic        : Idle 'config' mode
+ * [    1] - rs        : Reset
+ * [    0] - ru        : Running
+```
+
+
+### Command Execute (Write only addr `0x01`)
+
+```text
+,-----------------------------------------------------------------------------------------------,
+|31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0|
+|-----------------------------------------------------------------------------------------------|
+|                             /                             |    len    |    lat    |  /  |as|rw|
+'-----------------------------------------------------------------------------------------------'
+
+ * [11: 8] - len : Length ( # of xfer - 1 )
+ * [ 7: 4] - lat : Latency counter
+ * [ 3: 2] - cs  : Chip Select
+ * [    1] - as  : Memory (0) / Register (1)
+ * [    0] - rw  : Write  (0) / Read     (1)
+```
+
+
+Word Queue Write
+----------------
+
+To put a word in the queue, write the attributes first, then write the
+corresponding data word. The write to the data register will trigger the
+queuing with whatever attributes were last set.
+
+The newly written word will always end up in last position (pos=2) and the
+words that were previously in positions 1 & 2 will be in position 0 & 1.
+
+
+### Word Enqueue - Data (Write only addr `0x02`)
+
+```text
+,-----------------------------------------------------------------------------------------------,
+|31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0|
+|-----------------------------------------------------------------------------------------------|
+|                                              data                                             |
+'-----------------------------------------------------------------------------------------------'
+
+ * [31: 0] - data : Data word to queue
+```
+
+
+### Word Enqueue - Attributes (Write only addr `0x03`)
+
+```text
+,-----------------------------------------------------------------------------------------------,
+|31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0|
+|-----------------------------------------------------------------------------------------------|
+|                                           /                                 |  oe |   rwds    |
+'-----------------------------------------------------------------------------------------------'
+
+ * [ 5: 4] - oe   : Output Enable (per 16 bits)
+ * [ 3: 0] - rwds : RWDS value     (per 8 bits)
+```
+
+
+Word Queue Read
+---------------
+
+To read a word from the queue, read the attributes first to get the RWDS
+values and then read the corresponding data word. The read of the data
+register will trigger the de-queuing.
+
+The words that is read is the one that was first, at position 0 in the queue.
+The words that were previously in positions 1 & 2 will be moved up to
+positions 0 & 1.
+
+
+### Word Dequeue - Data (Read only addr `0x02`)
+
+```text
+,-----------------------------------------------------------------------------------------------,
+|31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0|
+|-----------------------------------------------------------------------------------------------|
+|                                              data                                             |
+'-----------------------------------------------------------------------------------------------'
+
+ * [31: 0] - data : Data word to queue
+```
+
+
+### Word Dequeue - Attributes (Read only addr `0x03`)
+
+```text
+,-----------------------------------------------------------------------------------------------,
+|31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0|
+|-----------------------------------------------------------------------------------------------|
+|                                             /                                     |   rwds    |
+'-----------------------------------------------------------------------------------------------'
+
+ * [ 3: 0] - rwds : RWDS value     (per 8 bits)
+```

+ 76 - 0
cores/hyperram/rtl/hram_dline.v

@@ -0,0 +1,76 @@
+/*
+ * hram_dline.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 hram_dline #(
+	parameter integer N = 3
+)(
+	input  wire di,
+	output reg  do,
+	input  wire [N-1:0] delay,
+	input  wire clk
+);
+
+	genvar i;
+
+	// Signals
+	wire [N:0] stage;
+
+	// First stage input
+	assign stage[0] = di;
+
+	// Generate delays
+	generate
+		for (i=0; i<N; i=i+1)
+		begin
+			// Delay line
+			reg [(1<<i)-1:0] d;
+
+			if (i == 0)
+				always @(posedge clk)
+					d <= stage[i];
+			else
+				always @(posedge clk)
+					d <= { stage[i], d[(1<<i)-1:1] };
+
+			// Mux
+			assign stage[i+1] = delay[i] ? d[0] : stage[i];
+		end
+	endgenerate
+
+	// Final register
+	always @(posedge clk)
+		do <= stage[N];
+
+endmodule

+ 268 - 0
cores/hyperram/rtl/hram_phy_ice40.v

@@ -0,0 +1,268 @@
+/*
+ * hram_phy_ice40.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 hram_phy_ice40 #(
+	parameter integer SERDES_GRP_BASE = 0
+)(
+	// HyperRAM pins
+	inout  wire [7:0] hram_dq,
+	inout  wire       hram_rwds,
+	output wire       hram_ck,
+	output wire [3:0] hram_cs_n,
+	output wire       hram_rst_n,
+
+	// PHY interface
+	input  wire [ 1:0] phy_ck_en,
+
+	output wire [ 3:0] phy_rwds_in,
+	input  wire [ 3:0] phy_rwds_out,
+	input  wire [ 1:0] phy_rwds_oe,
+
+	output wire [31:0] phy_dq_in,
+	input  wire [31:0] phy_dq_out,
+	input  wire [ 1:0] phy_dq_oe,
+
+	input  wire [ 3:0] phy_cs_n,
+	input  wire        phy_rst_n,
+
+	// PHY configuration
+	input  wire [ 7:0] phy_cfg_wdata,
+	output wire [ 7:0] phy_cfg_rdata,
+	input  wire        phy_cfg_stb,
+
+	// Clocks / Sync
+	output reg  [ 3:0] clk_rd_delay,
+
+	input  wire clk_1x,
+	input  wire clk_4x,
+	input  wire clk_rd,
+	input  wire sync_4x,
+	input  wire sync_rd
+);
+
+	// Signals
+	// -------
+
+	reg        phy_edge;
+	reg  [1:0] phy_phase;
+
+	wire [1:0] serdes_ck_dout;
+
+	wire [1:0] serdes_rwds_din;
+	wire [1:0] serdes_rwds_dout;
+	wire [1:0] serdes_rwds_oe;
+
+	wire [1:0] serdes_dq_din[0:8];
+	wire [1:0] serdes_dq_dout[0:8];
+	wire [1:0] serdes_dq_oe[0:8];
+
+	reg  [3:0] iob_cs_n;
+
+
+	// Config
+	// ------
+
+	always @(posedge clk_1x)
+		if (phy_cfg_stb) begin
+			phy_edge     <= phy_cfg_wdata[6];
+			phy_phase    <= phy_cfg_wdata[5:4];
+			clk_rd_delay <= phy_cfg_wdata[3:0];
+		end
+
+
+	assign phy_cfg_rdata = {
+		1'b0,
+		phy_edge,
+		phy_phase,
+		clk_rd_delay
+	};
+
+
+	// Clock
+	// -----
+
+	ice40_oserdes #(
+		.MODE("CLK90_2X"),
+		.SERDES_GRP(SERDES_GRP_BASE + 'h90)
+	) oserdes_ck_I (
+		.d({2'b00, phy_ck_en}),
+		.q(serdes_ck_dout),
+		.sync(sync_4x),
+		.clk_1x(clk_1x),
+		.clk_4x(clk_4x)
+	);
+
+	SB_IO #(
+		.PIN_TYPE(6'b1100_01)
+	) io_ck_I (
+		.PACKAGE_PIN(hram_ck),
+		.OUTPUT_ENABLE(1'b1),
+		.D_OUT_0(serdes_ck_dout[0]),
+		.D_OUT_1(serdes_ck_dout[1]),
+		.OUTPUT_CLK(clk_4x)
+	);
+
+
+	// RWDS
+	// ----
+
+	ice40_oserdes #(
+		.MODE("DATA"),
+		.SERDES_GRP(SERDES_GRP_BASE + 'h80)
+	) oserdes_rwds_o_I (
+		.d(phy_rwds_out),
+		.q(serdes_rwds_dout),
+		.sync(sync_4x),
+		.clk_1x(clk_1x),
+		.clk_4x(clk_4x)
+	);
+
+	ice40_oserdes #(
+		.MODE("DATA"),
+		.SERDES_GRP(SERDES_GRP_BASE + 'h81)
+	) oserdes_rwds_oe_I (
+		.d({phy_rwds_oe[1], phy_rwds_oe[1], phy_rwds_oe[0], phy_rwds_oe[0]}),
+		.q(serdes_rwds_oe),
+		.sync(sync_4x),
+		.clk_1x(clk_1x),
+		.clk_4x(clk_4x)
+	);
+
+	ice40_iserdes #(
+		.EDGE_SEL("DUAL_POS_POS"),
+		.PHASE_SEL("DYNAMIC"),
+		.SERDES_GRP(SERDES_GRP_BASE + 'h80)
+	) iserdes_rwds_I (
+		.d(serdes_rwds_din),
+		.q(phy_rwds_in),
+		.edge_sel(phy_edge),
+		.phase_sel(phy_phase),
+		.sync(sync_rd),
+		.clk_1x(clk_1x),
+		.clk_4x(clk_rd)
+	);
+
+	SB_IO #(
+		.PIN_TYPE(6'b 1101_00)
+	) io_rwds_I (
+		.PACKAGE_PIN(hram_rwds),
+		.OUTPUT_ENABLE(serdes_rwds_oe[0]),
+		.D_OUT_0(serdes_rwds_dout[0]),
+		.D_IN_0(serdes_rwds_din[0]),
+		.D_IN_1(serdes_rwds_din[1]),
+		.OUTPUT_CLK(clk_4x),
+		.INPUT_CLK(clk_rd)
+	);
+
+
+	// DQ
+	// --
+
+	generate
+		genvar i;
+
+		for (i=0; i<8; i=i+1)
+		begin
+
+			ice40_oserdes #(
+				.MODE("DATA"),
+				.SERDES_GRP(SERDES_GRP_BASE + (i<<4))
+			) oserdes_dq_o_I (
+				.d({phy_dq_out[24+i], phy_dq_out[16+i], phy_dq_out[8+i], phy_dq_out[i]}),
+				.q(serdes_dq_dout[i]),
+				.sync(sync_4x),
+				.clk_1x(clk_1x),
+				.clk_4x(clk_4x)
+			);
+
+			ice40_oserdes #(
+				.MODE("DATA"),
+				.SERDES_GRP(SERDES_GRP_BASE + (i<<4) + 1)
+			) oserdes_dq_oe_I (
+				.d({phy_dq_oe[1], phy_dq_oe[1], phy_dq_oe[0], phy_dq_oe[0]}),
+				.q(serdes_dq_oe[i]),
+				.sync(sync_4x),
+				.clk_1x(clk_1x),
+				.clk_4x(clk_4x)
+			);
+
+			ice40_iserdes #(
+				.EDGE_SEL("DUAL_POS_POS"),
+				.PHASE_SEL("DYNAMIC"),
+				.SERDES_GRP(SERDES_GRP_BASE + (i<<4))
+			) iserdes_dq_I (
+				.d(serdes_dq_din[i]),
+				.q({phy_dq_in[24+i], phy_dq_in[16+i], phy_dq_in[8+i], phy_dq_in[i]}),
+				.edge_sel(phy_edge),
+				.phase_sel(phy_phase),
+				.sync(sync_rd),
+				.clk_1x(clk_1x),
+				.clk_4x(clk_rd)
+			);
+
+			SB_IO #(
+				.PIN_TYPE(6'b 1101_00)
+			) io_dq_I (
+				.PACKAGE_PIN(hram_dq[i]),
+				.OUTPUT_ENABLE(serdes_dq_oe[i][0]),
+				.D_OUT_0(serdes_dq_dout[i][0]),
+				.D_IN_0(serdes_dq_din[i][0]),
+				.D_IN_1(serdes_dq_din[i][1]),
+				.OUTPUT_CLK(clk_4x),
+				.INPUT_CLK(clk_rd)
+			);
+
+		end
+	endgenerate
+
+
+	// Aux signals
+	// -----------
+
+	always @(posedge clk_1x)
+		iob_cs_n <= phy_cs_n;
+
+	SB_IO #(
+		.PIN_TYPE(6'b 1101_01)
+	) io_cs_n_I[3:0] (
+		.PACKAGE_PIN(hram_cs_n),
+		.OUTPUT_ENABLE(1'b1),
+		.D_OUT_0(iob_cs_n),
+		.OUTPUT_CLK(clk_4x)
+	);
+
+	assign hram_rst_n = phy_rst_n;
+
+endmodule

+ 481 - 0
cores/hyperram/rtl/hram_top.v

@@ -0,0 +1,481 @@
+/*
+ * hram_top.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 hram_top (
+	// PHY interface
+	output reg  [ 1:0] phy_ck_en,
+
+	input  wire [ 3:0] phy_rwds_in,
+	output reg  [ 3:0] phy_rwds_out,
+	output reg  [ 1:0] phy_rwds_oe,
+
+	input  wire [31:0] phy_dq_in,
+	output reg  [31:0] phy_dq_out,
+	output reg  [ 1:0] phy_dq_oe,
+
+	output reg  [ 3:0] phy_cs_n,
+	output wire        phy_rst_n,
+
+	// PHY configuration
+	output wire [ 7:0] phy_cfg_wdata,
+	input  wire [ 7:0] phy_cfg_rdata,
+	output wire        phy_cfg_stb,
+
+	// Memory interface
+	input  wire [ 1:0] mi_addr_cs,
+	input  wire [31:0] mi_addr,
+	input  wire [ 6:0] mi_len,
+	input  wire        mi_rw,		/* 0=Write, 1=Read */
+	input  wire        mi_linear,	/* 0=Wrapped burst, 1=Linear */
+	input  wire        mi_valid,
+	output wire        mi_ready,
+
+	input  wire [31:0] mi_wdata,
+	input  wire [ 3:0] mi_wmsk,
+	output wire        mi_wack,
+	output wire        mi_wlast,
+
+	output wire [31:0] mi_rdata,
+	output wire        mi_rstb,
+	output wire        mi_rlast,
+
+	// Wishbone interface
+	input  wire [31:0] wb_wdata,
+	output reg  [31:0] wb_rdata,
+	input  wire [ 3:0] wb_addr,
+	input  wire        wb_we,
+	input  wire        wb_cyc,
+	output wire        wb_ack,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	// FSM
+	// ---
+
+	localparam
+		ST_IDLE_CFG		= 0,
+		ST_IDLE_RUN		= 1,
+		ST_CMD_ADDR_MSB	= 2,
+		ST_CMD_ADDR_LSB	= 3,
+		ST_LATENCY		= 4,
+		ST_DATA_WRITE	= 5,
+		ST_DATA_READ	= 6,
+		ST_DONE			= 7;
+
+	reg [3:0] state;
+	reg [3:0] state_nxt;
+
+
+	// Signals
+	// -------
+
+	// Control
+	wire running;
+
+	reg  [ 3:0] lat_cnt;
+	wire        lat_last;
+
+	reg  [ 7:0] xfer_cnt;
+	wire        xfer_last;
+
+	reg  [95:0] sr_data;
+	reg  [11:0] sr_mask;
+	reg  [ 5:0] sr_oe;
+	reg  [ 1:0] sr_src;
+	reg  [ 1:0] sr_ce;
+
+	wire [ 1:0] cap_in;
+	wire [ 1:0] cap_out;
+
+	// Current transaction
+	reg         cmd_is_read;
+	reg         cmd_is_reg;
+	reg         cmd_is_wb;
+	reg  [ 3:0] cmd_cs;
+
+	// Wishbone interface
+	reg         wb_ack_i;
+
+	reg         wbi_we_csr;
+	reg         wbi_we_exec;
+	reg         wbi_we_wq_data;
+	reg         wbi_ae_wq_data;
+	reg         wbi_we_wq_attr;
+
+	wire        wbi_cmd_now;
+	wire  [3:0] wbi_cmd_len;
+	wire  [3:0] wbi_cmd_lat;
+	wire  [1:0] wbi_cmd_cs;
+	wire        wbi_cmd_is_reg;
+	wire        wbi_cmd_is_read;
+
+	reg  [15:0] wbi_csr;
+	wire [31:0] wbi_csr_rd;
+	reg  [ 5:0] wbi_attr;
+
+
+	// FSM
+	// ---
+
+	// State register
+	always @(posedge clk)
+		if (rst)
+			state <= ST_IDLE_CFG;
+		else
+			state <= state_nxt;
+
+	// Next-State logic
+	always @(*)
+	begin
+		// Default is to stay put
+		state_nxt = state;
+
+		// Transisions
+		case (state)
+			ST_IDLE_CFG:
+				if (wbi_cmd_now)
+					state_nxt = ST_CMD_ADDR_MSB;
+				else if (running)
+					state_nxt = ST_IDLE_RUN;
+
+			ST_IDLE_RUN:
+				if (mi_valid)
+					state_nxt = ST_CMD_ADDR_MSB;
+				else if (!running)
+					state_nxt = ST_IDLE_CFG;
+
+			ST_CMD_ADDR_MSB:
+				state_nxt = ST_CMD_ADDR_LSB;
+
+			ST_CMD_ADDR_LSB:
+				state_nxt = (cmd_is_reg & ~cmd_is_read) ? ST_DONE : ST_LATENCY;
+
+			ST_LATENCY:
+				if (lat_last)
+					state_nxt = cmd_is_read ? ST_DATA_READ : ST_DATA_WRITE;
+
+			ST_DATA_WRITE:
+				if (xfer_last)
+					state_nxt = ST_DONE;
+
+			ST_DATA_READ:
+				if (xfer_last)
+					state_nxt = ST_DONE;
+
+			ST_DONE:
+				state_nxt = running ? ST_IDLE_RUN : ST_IDLE_CFG;
+		endcase
+	end
+
+
+	// Control
+	// -------
+
+	// State
+	assign running = wbi_csr[0];
+
+	// Command latch
+	always @(posedge clk)
+	begin
+		if ((state == ST_IDLE_RUN) & mi_valid)
+		begin
+			cmd_is_read <= mi_rw;
+			cmd_is_reg  <= 1'b0;
+			cmd_is_wb   <= 1'b0;
+			cmd_cs      <= 4'hf ^ (1 << mi_addr_cs);
+		end
+		else if ((state == ST_IDLE_CFG) & wbi_cmd_now)
+		begin
+			cmd_is_read <= wbi_cmd_is_read;
+			cmd_is_reg  <= wbi_cmd_is_reg;
+			cmd_is_wb   <= 1'b1;
+			cmd_cs      <= 4'hf ^ (1 << wbi_cmd_cs);
+		end
+	end
+
+	// Shift register control
+	always @(*)
+	begin
+		// Defaults
+		sr_ce[1]  = 1'b0;
+		sr_ce[0]  = 1'b0;
+		sr_src[1] = 1'b0;
+		sr_src[0] = 1'b0;
+
+		// Memory interface Command accept
+		if ((state == ST_IDLE_RUN) & mi_valid)
+		begin
+			sr_ce[1]  = 1'b1;
+			sr_src[1] = 1'b1;
+		end
+
+		// Wishbone accesses
+		if (wbi_ae_wq_data)
+		begin
+			sr_ce[1]  = 1'b1;
+			sr_ce[0]  = 1'b1;
+			sr_src[1] = 1'b0;
+			sr_src[0] = 1'b1;
+		end
+
+		// Config mode capture
+		if (cap_out == 2'b01)
+		begin
+			sr_ce[1]  = 1'b1;
+			sr_ce[0]  = 1'b1;
+			sr_src[1] = 1'b0;
+			sr_src[0] = 1'b0;
+		end
+
+		// Normal "shift"
+		if ((state == ST_CMD_ADDR_MSB) || (state == ST_CMD_ADDR_LSB))
+		begin
+			sr_ce[1]  = 1'b1;
+			sr_ce[0]  = 1'b1;
+			sr_src[1] = 1'b0;
+			sr_src[0] = 1'b0;
+		end
+	end
+
+	// Shift register
+	always @(posedge clk)
+	begin
+		// MSBs [95:32]
+		if (sr_ce[1])
+		begin
+			sr_oe  [ 5: 2] <= sr_src[1] ? 4'b1110 : sr_oe  [3:0];
+			sr_mask[11: 4] <= sr_src[1] ? 8'h00   : sr_mask[7:0];
+			sr_data[95:32] <= sr_src[1] ?
+				{ mi_rw, 1'b0, mi_linear, mi_addr[31:3], 13'h0000, mi_addr[2:0], 16'h0000 } :
+				sr_data[63:0];
+		end
+
+		// LSBs [31: 0]
+		if (sr_ce[0])
+		begin
+			sr_oe  [ 1:0] <= sr_src[0] ? wbi_attr[5:4] : 2'b11;
+			sr_mask[ 3:0] <= sr_src[0] ? wbi_attr[3:0] : phy_rwds_in;
+			sr_data[31:0] <= sr_src[0] ? wb_wdata      : phy_dq_in;
+		end
+	end
+
+	// Latency counter
+	always @(posedge clk)
+	begin
+		if (state == ST_IDLE_RUN)
+			lat_cnt <= wbi_csr[11:8] - 1;
+		else if (state == ST_IDLE_CFG)
+			lat_cnt <= wbi_cmd_lat - 1;
+		else if (state == ST_LATENCY)
+			lat_cnt <= lat_cnt - 1;
+	end
+
+	assign lat_last = lat_cnt[3];
+
+	// Transfer counter
+	always @(posedge clk)
+	begin
+		if (state == ST_IDLE_RUN)
+			xfer_cnt <= { 1'b0, mi_len } - 1;
+		else if (state == ST_IDLE_CFG)
+			xfer_cnt <= { 4'h0, wbi_cmd_len } - 1;
+		else if ((state == ST_DATA_WRITE) || (state == ST_DATA_READ))
+			xfer_cnt <= xfer_cnt - 1;
+	end
+
+	assign xfer_last = xfer_cnt[7];
+
+	// Input capture
+		// 00 - Nothing
+		// 01 - Capture WB
+		// 10 - Capture MemIF
+		// 11 - Capture MemIF last
+	assign cap_in[1] = (state == ST_DATA_READ) & ~cmd_is_wb;
+	assign cap_in[0] = (state == ST_DATA_READ) & (cmd_is_wb | xfer_last);
+
+	hram_dline #(
+		.N(3)
+	) cap_I[1:0] (
+		.di(cap_in),
+		.do(cap_out),
+		.delay(wbi_csr[14:12]),
+		.clk(clk)
+	);
+
+
+	// PHY drive
+	// ---------
+
+	// Main signals
+	always @(*)
+	begin
+		// Defaults
+		phy_ck_en    = 2'b00;
+		phy_rwds_out = 4'h0;
+		phy_rwds_oe  = 2'b00;
+		phy_dq_out   = sr_data[95:64];
+		phy_dq_oe    = 2'b00;
+		phy_cs_n     = 4'hf;
+
+		// Special per-state overrides
+		case (state)
+			ST_CMD_ADDR_MSB: begin
+				phy_ck_en    = 2'b11;
+				phy_dq_oe    = sr_oe[5:4];
+				phy_cs_n     = cmd_cs;
+			end
+
+			ST_CMD_ADDR_LSB: begin
+				phy_ck_en    = 2'b11;
+				phy_dq_oe    = sr_oe[5:4];
+				phy_cs_n     = cmd_cs;
+			end
+
+			ST_LATENCY: begin
+				phy_ck_en    = 2'b11;
+				phy_cs_n     = cmd_cs;
+			end
+
+			ST_DATA_WRITE: begin
+				phy_ck_en    = 2'b11;
+				phy_dq_oe    = 2'b11;
+				phy_rwds_oe  = 2'b11;
+				phy_dq_out   = cmd_is_wb ? sr_data[95:64] : mi_wdata;
+				phy_rwds_out = cmd_is_wb ? sr_mask[11: 8] : mi_wmsk;
+				phy_cs_n     = cmd_cs;
+			end
+
+			ST_DATA_READ: begin
+				phy_ck_en    = 2'b11;
+				phy_cs_n     = cmd_cs;
+			end
+
+			ST_DONE: begin
+				phy_cs_n     = cmd_cs;
+			end
+		endcase
+	end
+
+	// OOB
+	assign phy_rst_n = ~wbi_csr[1];
+
+
+	// Memory interface
+	// ----------------
+
+	assign mi_ready = (state == ST_IDLE_RUN);
+	assign mi_wack  = (state == ST_DATA_WRITE) & ~cmd_is_wb;
+	assign mi_wlast = xfer_last;
+
+	assign mi_rdata = phy_dq_in;
+	assign mi_rstb  = cap_out[1];
+	assign mi_rlast = cap_out[0];
+
+
+	// Wishbone interface
+	// ------------------
+
+	// Ack
+	always @(posedge clk)
+		wb_ack_i <= wb_cyc & ~wb_ack_i;
+
+	assign wb_ack = wb_ack_i;
+
+	// Read Mux
+	always @(posedge clk)
+		if (~wb_cyc | wb_ack)
+			wb_rdata <= 32'h00000000;
+		else
+			case (wb_addr[1:0])
+				2'b00:   wb_rdata <= wbi_csr_rd;
+				2'b10:   wb_rdata <= sr_data[95:64];
+				2'b11:   wb_rdata <= { 26'h0000000, sr_oe[5:4], sr_mask[11:8] };
+				default: wb_rdata <= 32'hxxxxxxxx;
+			endcase
+
+	assign wbi_csr_rd[31:16] = { 8'h00, phy_cfg_rdata };
+	assign wbi_csr_rd[15: 0] = (wbi_csr & 16'hff03) | {
+				12'h000,
+				(state == ST_IDLE_RUN),
+				(state == ST_IDLE_CFG),
+				2'b00
+			};
+
+	// Read/Write/Access Enables
+	always @(posedge clk)
+	begin
+		if (wb_ack) begin
+			wbi_we_csr     <= 1'b0;
+			wbi_we_exec    <= 1'b0;
+			wbi_we_wq_data <= 1'b0;
+			wbi_ae_wq_data <= 1'b0;
+			wbi_we_wq_attr <= 1'b0;
+		end else begin
+			wbi_we_csr     <= wb_cyc & wb_we & (wb_addr[1:0] == 2'b00);
+			wbi_we_exec    <= wb_cyc & wb_we & (wb_addr[1:0] == 2'b01);
+			wbi_we_wq_data <= wb_cyc & wb_we & (wb_addr[1:0] == 2'b10);
+			wbi_ae_wq_data <= wb_cyc &         (wb_addr[1:0] == 2'b10);
+			wbi_we_wq_attr <= wb_cyc & wb_we & (wb_addr[1:0] == 2'b11);
+		end
+	end
+
+	// CSR
+	always @(posedge clk)
+		if (rst)
+			wbi_csr <= 16'h0000;
+		else if (wbi_we_csr)
+			wbi_csr <= wb_wdata[15:0];
+
+	// PHY config
+	assign phy_cfg_wdata = wb_wdata[23:16];
+	assign phy_cfg_stb   = wbi_we_csr;
+
+	// Attrs
+	always @(posedge clk)
+		if (wbi_we_wq_attr)
+			wbi_attr <= wb_wdata[5:0];
+
+	// Command execute
+	assign wbi_cmd_now     = wbi_we_exec;
+	assign wbi_cmd_len     = wb_wdata[11:8];
+	assign wbi_cmd_lat     = wb_wdata[ 7:4];
+	assign wbi_cmd_cs      = wb_wdata[ 3:2];
+	assign wbi_cmd_is_reg  = wb_wdata[1];
+	assign wbi_cmd_is_read = wb_wdata[0];
+
+endmodule

+ 319 - 0
cores/hyperram/sim/hram_top_tb.v

@@ -0,0 +1,319 @@
+/*
+ * hram_top_tb.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
+`timescale 1ns / 100ps
+
+module hram_top_tb;
+
+	// Signals
+	// -------
+
+	// HyperRAM pins
+	wire [7:0] hram_dq;
+	wire       hram_rwds;
+	wire       hram_ck;
+	wire [3:0] hram_cs_n;
+	wire       hram_rst_n;
+
+	// Memory interface
+	wire [ 1:0] mi_addr_cs;
+	reg  [31:0] mi_addr;
+	reg  [ 6:0] mi_len;
+	reg         mi_rw;
+	wire        mi_linear;
+	reg         mi_valid;
+	wire        mi_ready;
+
+	reg  [31:0] mi_wdata;
+	wire [ 3:0] mi_wmsk;
+	wire        mi_wack;
+
+	wire [31:0] mi_rdata;
+	wire        mi_rstb;
+
+	// Wishbone interface
+	reg  [31:0] wb_wdata;
+	wire [31:0] wb_rdata;
+	reg  [ 3:0] wb_addr;
+	reg         wb_we;
+	reg         wb_cyc;
+	wire        wb_ack;
+
+	// Clocks / Sync
+	wire [3:0] clk_read_delay;
+
+	reg  pll_lock = 1'b0;
+	wire clk_slow;
+	reg  clk_fast = 1'b0;
+	reg  clk_read = 1'b0;
+	reg  clk_sync;
+	wire rst;
+
+	reg        rst_div;
+	reg  [1:0] clk_div;
+	reg  [3:0] rst_cnt = 4'h8;
+
+
+	// Recording setup
+	// ---------------
+
+	initial begin
+		$dumpfile("hram_top_tb.vcd");
+		$dumpvars(0,hram_top_tb);
+	end
+
+
+	// DUT
+	// ---
+
+	hram_top dut_I (
+		.hram_dq(hram_dq),
+		.hram_rwds(hram_rwds),
+		.hram_ck(hram_ck),
+		.hram_cs_n(hram_cs_n),
+		.hram_rst_n(hram_rst_n),
+		.mi_addr_cs(mi_addr_cs),
+		.mi_addr(mi_addr),
+		.mi_len(mi_len),
+		.mi_rw(mi_rw),
+		.mi_linear(mi_linear),
+		.mi_valid(mi_valid),
+		.mi_ready(mi_ready),
+		.mi_wdata(mi_wdata),
+		.mi_wmsk(mi_wmsk),
+		.mi_wack(mi_wack),
+		.mi_rdata(mi_rdata),
+		.mi_rstb(mi_rstb),
+		.wb_wdata(wb_wdata),
+		.wb_rdata(wb_rdata),
+		.wb_addr(wb_addr),
+		.wb_we(wb_we),
+		.wb_cyc(wb_cyc),
+		.wb_ack(wb_ack),
+		.clk_read_delay(clk_read_delay),
+		.clk_slow(clk_slow),
+		.clk_fast(clk_fast),
+		.clk_read(clk_read),
+		.clk_sync(clk_sync),
+		.rst(rst)
+	);
+
+
+	// Mem interface
+	// -------------
+
+	// Fixed values
+	assign mi_addr_cs = 2'b01;
+	assign mi_linear  = 1'b0;
+	assign mi_wmsk    = 4'h0;
+
+	always @(posedge clk_slow)
+		if (rst)
+			mi_wdata <= 32'h00010203;
+		else if (mi_wack)
+			mi_wdata <= mi_wdata + 32'h04040404;
+
+	// Stimulus
+	// --------
+
+	task wb_write;
+		input [ 3:0] addr;
+		input [31:0] data;
+		begin
+			wb_addr  <= addr;
+			wb_wdata <= data;
+			wb_we    <= 1'b1;
+			wb_cyc   <= 1'b1;
+
+			while (~wb_ack)
+				@(posedge clk_slow);
+
+			wb_addr  <= 4'hx;
+			wb_wdata <= 32'hxxxxxxxx;
+			wb_we    <= 1'bx;
+			wb_cyc   <= 1'b0;
+
+			@(posedge clk_slow);
+		end
+	endtask
+
+	task mi_burst_write;
+		input [31:0] addr;
+		input [ 6:0] len;
+		begin
+			mi_addr  <= addr;
+			mi_len   <= len;
+			mi_rw    <= 1'b0;
+			mi_valid <= 1'b1;
+
+			@(posedge clk_slow);
+			while (~mi_ready)
+				@(posedge clk_slow);
+
+			mi_valid <= 1'b0;
+
+			@(posedge clk_slow);
+		end
+	endtask
+
+	task mi_burst_read;
+		input [31:0] addr;
+		input [ 6:0] len;
+		begin
+			mi_addr  <= addr;
+			mi_len   <= len;
+			mi_rw    <= 1'b1;
+			mi_valid <= 1'b1;
+
+			@(posedge clk_slow);
+			while (~mi_ready)
+				@(posedge clk_slow);
+
+			mi_valid <= 1'b0;
+
+			@(posedge clk_slow);
+		end
+	endtask
+
+	initial begin
+		// Defaults
+		wb_addr  <= 4'hx;
+		wb_wdata <= 32'hxxxxxxxx;
+		wb_we    <= 1'bx;
+		wb_cyc   <= 1'b0;
+
+		mi_addr  <= 32'hxxxxxxxx;
+		mi_len   <= 7'hx;
+		mi_rw    <= 1'bx;
+		mi_valid <= 1'b0;
+
+		@(negedge rst);
+		@(posedge clk_slow);
+
+		// Reset pulse
+		wb_write(4'h0, 32'h00001102);
+		wb_write(4'h0, 32'h00001100);
+
+		// Queue CR0 write
+		wb_write(4'h3, 32'h00000030);
+		wb_write(4'h2, 32'h60000100);
+		wb_write(4'h2, 32'h00008fef);
+		wb_write(4'h2, 32'h00000000);
+
+		wb_write(4'h1, 32'h0000000e);
+
+		// Wait
+		#200
+		@(posedge clk_slow);
+
+		// Queue Memory write
+		wb_write(4'h3, 32'h00000030);
+		wb_write(4'h2, 32'h00000246);
+		wb_write(4'h3, 32'h00000020);
+		wb_write(4'h2, 32'h00040000);
+		wb_write(4'h3, 32'h00000030);
+		wb_write(4'h2, 32'hcafebabe);
+
+		wb_write(4'h1, 32'h0000021c);
+
+		// Wait
+		#200
+		@(posedge clk_slow);
+
+		// Queue Memory read
+		wb_write(4'h3, 32'h00000030);
+		wb_write(4'h2, 32'h80000246);
+		wb_write(4'h3, 32'h00000020);
+		wb_write(4'h2, 32'h00040000);
+		wb_write(4'h3, 32'h00000000);
+		wb_write(4'h2, 32'h00000000);
+
+		wb_write(4'h1, 32'h0000021d);
+
+		// Wait
+		#200
+		@(posedge clk_slow);
+
+		// Switch to run-time mode
+		wb_write(4'h0, 32'h00001101);
+
+		// Execute 32 byte burst
+		mi_burst_write(32'h00002000, 7'd31);
+		mi_burst_read (32'h00002000, 7'd15);
+		mi_burst_write(32'h00003000, 7'd31);
+	end
+
+
+	// Clock / Reset
+	// -------------
+
+	// Native clocks
+	initial begin
+		# 200 pll_lock = 1'b1;
+		# 100000 $finish;
+	end
+
+	always #4 clk_fast = ~clk_fast;		// 125   MHz
+	always #8 clk_read = ~clk_read;		//  62.5 MHz
+
+	// Clock Divider & Sync
+	always @(negedge clk_read or negedge pll_lock)
+		if (~pll_lock)
+			rst_div <= 1'b1;
+		else
+			rst_div <= 1'b0;
+
+	always @(posedge clk_fast or posedge rst_div)
+		if (rst_div)
+			{ clk_sync, clk_div } <= 3'b000;
+		else
+			case (clk_div)
+				2'b00: { clk_sync, clk_div } <= 3'b001;
+				2'b01: { clk_sync, clk_div } <= 3'b010;
+				2'b10: { clk_sync, clk_div } <= 3'b011;
+				2'b11: { clk_sync, clk_div } <= 3'b100;
+			endcase
+
+	assign clk_slow = clk_div[1];
+
+	// Reset
+	always @(posedge clk_slow or negedge pll_lock)
+		if (~pll_lock)
+			rst_cnt <= 4'h8;
+		else if (rst_cnt[3])
+			rst_cnt <= rst_cnt + 1;
+
+	assign rst = rst_cnt[3];
+
+endmodule

+ 114 - 0
cores/hyperram/sw/serdes-nextpnr-place.py

@@ -0,0 +1,114 @@
+#!/usr/bin/env python
+
+from collections import namedtuple
+import re
+
+BEL = namedtuple('BEL', 'x y z')
+
+def to_int(s):
+	return int(re.sub(r'[^\d-]+', '', s))
+
+def split_bel(b):
+	return BEL(*[to_int(x) for x in b.split('/', 3)])
+
+def find_io_site(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 split_bel(p.cell.attrs['BEL'])
+	return None
+
+
+# Find all groups and all LCs
+serdes_lcs = {}
+serdes_site = {}
+
+for n,c in ctx.cells:
+	if 'SERDES_GRP' in c.attrs:
+		# Get group ID
+		grp = int(c.attrs['SERDES_GRP'],2)
+
+		# Append to LCs list and IO site list
+		serdes_lcs.setdefault(grp, []).append(c)
+
+		io_site = find_io_site(c)
+		if io_site is not None:
+			if (grp in serdes_site) and (serdes_site[grp] != io_site):
+				raise RuntimeError('IO site conflict for SERDES group %d (%s vs %s)' % (grp, io_site, serdes_site[grp]))
+			serdes_site[grp] = io_site
+
+# Split into top / bottmon IO banks
+serdes_top = {}
+serdes_bot = {}
+
+for grp, site in serdes_site.items():
+	if site.y == 31:
+		serdes_top[grp] = site
+	else:
+		serdes_bot[grp] = site
+
+# Place them
+# (super dumb algo ...)
+def place(sites):
+	# Init set
+	toplace = sorted(sites.items(), key=lambda x:-x[1].x)
+	placed  = {}
+
+	# Scan each possible site in order and place ASAP
+	for x in range(1,26):
+		# Skip invalid (SPRAM columns)
+		if x in [6, 19]:
+			continue
+
+		# Place next one ?
+		if x >= (toplace[-1][1].x - 1):
+			placed[toplace.pop()[0]] = x
+
+		# Done ?
+		if not toplace:
+			break
+
+	# Cleanup pass
+	while True:
+		# Find a group that could be moved to its preferred X
+		used = set(placed.values())
+		for grp, pos in placed.items():
+			px = sites[grp].x
+			if (pos != px) and (px not in used) and (px not in [6, 19]):
+				placed[grp] = sites[grp].x
+				break
+		else:
+			break
+
+	# Done
+	return placed
+
+serdes_top_place = place(serdes_top)
+serdes_bot_place = place(serdes_bot)
+
+# Merge results
+serdes_place = dict()
+serdes_place.update(serdes_top_place)
+serdes_place.update(serdes_bot_place)
+
+# Add the final BEL attribute to all LCs
+for grp in serdes_place.keys():
+	x = serdes_place[grp]
+	for lc in serdes_lcs[grp]:
+		# Grab attributes
+		yofs = int(lc.attrs['SERDES_YOFS'], 2)
+		y = (30-yofs) if serdes_site[grp].y == 31 else (1+yofs)
+		z = int(lc.attrs['SERDES_Z'], 2)
+
+		# Set attribute
+		lc.setAttr('BEL', 'X%d/Y%d/lc%d' % (x, y, z))
+
+		# Clear out
+		lc.unsetAttr('SERDES_GRP')
+		lc.unsetAttr('SERDES_YOFS')
+		lc.unsetAttr('SERDES_Z')