Thankyou for the reply, good to know i was at least right about the timings.
I have not experienced any issues with the current implementation yet, but that will likely be due to luck more than anything. By the time im done id really like to have the "perfect" sram controller that i can just rely on in future projects.
Took quite a few hours last night to simulate the situation you described but i managed to model it in a test. So now i have a specific test case to work against.

around t=149.5ns
sram_addr changes from 0x00200 to 0x00300 tooo early.
0.5ns later, clk goes high and sram_we_n goes high
BUT...
But the sram_addr has already seen the new address 0x00300 during the write
Implementing your changes:
This is looking good to me.
I cant simulate data coming early because its now latched on the negative edge outside the controller, which i guess is the point.
I left out this change
assign sram_io = (wr_en && !sram_we) ? data_in : 12'bzzzzzzzzzzzz;
as it seemed to cause issues with the hold time, but i cant be certain.
This gives me a controller once adding in the rest of the signals and some sensible renaming as
module sram #(
parameter ADDR_WIDTH = 18, // 256K addresses
parameter DATA_WIDTH = 16 // 16-bit data
)(
input wire clk,
input wire rst_n,
// User interface
input wire [ADDR_WIDTH-1:0] wr_addr,
input wire [ADDR_WIDTH-1:0] rd_addr,
input wire [DATA_WIDTH-1:0] wr_data,
input wire wr_en,
input wire rd_en,
input wire [1:0] byte_en, // [1]=upper byte, [0]=lower byte
output reg [DATA_WIDTH-1:0] rd_data,
// SRAM chip interface
output wire [ADDR_WIDTH-1:0] sram_addr,
inout wire [DATA_WIDTH-1:0] sram_data,
output wire sram_ce_n, // Chip Enable (active low)
output wire sram_oe_n, // Output Enable (active low)
output wire sram_we_n, // Write Enable (active low)
output wire sram_ub_n, // Upper Byte Enable (active low)
output wire sram_lb_n // Lower Byte Enable (active low)
);
// Set the SRAM address to either the write or read address
assign sram_addr = wr_en ? wr_addr : rd_addr;
// Set the SRAM data to wr_data if writing, otherwise tristate
assign sram_data = wr_en ? wr_data : {DATA_WIDTH{1'bz}};
// Chip enable always active
assign sram_ce_n = 1'b0;
// Output enable always active
// This could have been: assign sram_oe_n = ~rd_en; but the tristating should cover me?
assign sram_oe_n = 1'b0;
// Write enable: low during HIGH clock phase when writing
// When using I need to update wr_addr, wr_data, and wr_en at negedge to ensure
// signals are stable during the write pulse (HIGH phase)
assign sram_we_n = ~(wr_en & clk);
// Byte enables (active low) i dont need these but probably should allow them.
assign sram_ub_n = ~byte_en[1];
assign sram_lb_n = ~byte_en[0];
// Latch read data on clock edge when not writing
always @(posedge clk) begin
if (!wr_en)
rd_data <= sram_data;
end
endmodule
Ill try this out on hardware later and see what happens, i have learnt loads during the night.
Just want to say thanks for your patience and help as always.