// SPDX-License-Identifier: AGPL-3.0-Only
/*
 * Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
 */

`include "common.vh"

module mdio (
	input clk,
	input ce,
	input mdi,
	output reg mdo,
	output reg mdo_valid,

	input ack, err,
	output cyc,
	output reg stb, we,
	output reg [4:0] addr,
	output reg [15:0] data_write,
	input [15:0] data_read
);

	parameter [PHYAD_BITS-1:0] ADDRESS = 0;

	localparam IDLE		= 0;
	localparam PREAMBLE	= 1;
	localparam ST		= 2;
	localparam OP		= 3;
	localparam PHYAD	= 4;
	localparam REGAD	= 5;
	localparam TA		= 6;
	localparam DATA		= 7;

	localparam PREAMBLE_BITS	= 32;
	localparam OP_BITS		= 2;
	localparam PHYAD_BITS		= 5;
	localparam REGAD_BITS		= 5;
	localparam TA_BITS		= 2;
	localparam DATA_BITS		= 16;

	localparam OP_READ	= 2'b10;
	localparam OP_WRITE	= 2'b01;

	reg mdo_next, mdo_valid_next;
	reg stb_next, we_next;
	initial stb = 0;
	reg [4:0] addr_next;
	reg [15:0] data_next;

	reg bad, bad_next, saved_err, saved_err_next;
	reg [2:0] state, state_next;
	reg [4:0] state_counter, state_counter_next;
	initial state = IDLE;

	/*
	 * NB: stb_next and data_next are assigned to stb and data_write every
	 * clock, whereas the other signals are only assigned if ce is high.
	 * This ensures that no duplicate reads/writes are issued, and that
	 * data_read is sampled promptly. However, it also means that any
	 * assignments to these regs in the state machine must be qualified
	 * by ce.
	 */
	always @(*) begin
		mdo_next = 1'bX;
		mdo_valid_next = 0;

		stb_next = stb;
		we_next = we;
		addr_next = addr;
		data_next = data_write;
		saved_err_next = saved_err;
		if (stb && (ack || err)) begin
			stb_next = 0;
			if (err)
				saved_err_next = 1;
			else
				data_next = data_read;
		end

		state_next = state;
		state_counter_next = state_counter - 1;
		bad_next = bad;
		case (state)
		IDLE: begin
			bad_next = 0;
			state_counter_next = PREAMBLE_BITS - 1;
			if (mdi)
				state_next = PREAMBLE;
		end
		PREAMBLE: begin
			if (!state_counter)
				state_counter_next = 0;

			if (!mdi) begin
				if (state_counter)
					state_next = IDLE;
				else
					state_next = ST;
			end
		end
		ST: begin
			if (!mdi)
				bad_next = 1;
			state_next = OP;
			state_counter_next = OP_BITS - 1;
		end
		OP: begin
			/* This is a bit of an abuse of we :) */
			we_next = mdi;
			/* Accordingly, cancel any outstanding transactions */
			stb_next = 0;
			saved_err_next = 0;
			if (!state_counter) begin
				case ({ we, mdi })
				OP_READ: we_next = 0;
				OP_WRITE: we_next = 1;
				default: bad_next = 1;
				endcase

				state_next = PHYAD;
				state_counter_next = PHYAD_BITS - 1;
			end
		end
		PHYAD: begin
			if (mdi != ADDRESS[state_counter])
				bad_next = 1;

			if (!state_counter) begin
				state_next = REGAD;
				state_counter_next = REGAD_BITS - 1;
			end
		end
		REGAD: begin
			addr_next = { addr[3:0], mdi };

			if (!state_counter) begin
				if (ce && !we && !bad)
					stb_next = 1;

				state_next = TA;
				state_counter_next = TA_BITS - 1;
			end
		end
		TA: begin
			if (!state_counter) begin
				if (!we && !bad) begin
					mdo_next = 0;
					mdo_valid_next = 1;
					if (stb || saved_err) begin
						/* No response */
						if (ce)
							stb_next = 0;
						bad_next = 1;
						mdo_valid_next = 0;
					end
				end

				state_next = DATA;
				state_counter_next = DATA_BITS - 1;
			end
		end
		DATA: begin
			if (ce && we) begin
				data_next = { data_write[14:0], mdi };
			end else if (!bad) begin
				/* More data_write abuse */
				mdo_next = data_write[15];
				mdo_valid_next = 1;
				if (ce)
					data_next = { data_write[14:0], 1'bX };
			end

			if (!state_counter) begin
				if (ce && we && !bad)
					stb_next = 1;

				bad_next = 0;
				state_next = IDLE;
			end
		end
		endcase
	end

	always @(posedge clk) begin
		stb <= stb_next;
		data_write <= data_next;
		saved_err <= saved_err_next;
		if (ce) begin
			mdo <= mdo_next;
			mdo_valid <= mdo_valid_next;
			we <= we_next;
			addr <= addr_next;
			state <= state_next;
			state_counter <= state_counter_next;
			bad <= bad_next;
		end
	end

	/* No multi-beat transactions */
	assign cyc = stb;

`ifndef SYNTHESIS
	reg [255:0] state_text;

	always @(*) begin
		case (state)
		IDLE: state_text = "IDLE";
		PREAMBLE: state_text = "PREAMBLE";
		ST: state_text = "ST";
		OP: state_text = "OP";
		PHYAD: state_text = "PHYAD";
		REGAD: state_text = "REGAD";
		TA: state_text = "TA";
		DATA: state_text = "DATA";
		endcase
	end
`endif

endmodule