blob: 8181d6a6feb7398d001857f30ab2e1b6b43a5053 [file] [log] [blame]
// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
//
// Behavioral model of SPI Flash
program spiflash #(
parameter int unsigned FlashSize = 1024*1024, // 1MB
parameter int unsigned PageSize = 256, // Bytes
parameter bit EnFastSim = 1'b 1, // Reduce the latencies
// Timing information s_ ms_ us
// Program Latency
parameter int unsigned tPPTyp = 1_000, // us
parameter int unsigned tPPMax = 3_000, // us
// Sector Erase(4kB) Latency
parameter int unsigned tSETyp = 45_000, // us
parameter int unsigned tSEMax = 200_000, // us
// Block Erase 32kB Latency
parameter int unsigned tBE32Typ = 200_000, // us
parameter int unsigned tBE32Max = 1_000_000, // us
// Block Erase 64kB Latency
parameter int unsigned tBE64Typ = 400_000, // us
parameter int unsigned tBE64Max = 2_000_000, // us
// Chip Erase Latency
parameter int unsigned tCETyp = 200_000_000, // us
parameter int unsigned tCEMax = 320_000_000, // us
// Num of Repeating Continuous Code ('h7F)
parameter int unsigned CcNum = 7,
parameter logic [7:0] JedecId = 8'h 1F,
parameter logic [15:0] DeviceId = 16'h 1234,
// Command Support
parameter bit EnFastReadDual = 1'b 1,
parameter bit EnFastReadQuad = 1'b 1,
// Dummy Cycle
parameter int unsigned DummySizeNormal = 8,
parameter int unsigned DummySizeDual = 4,
parameter int unsigned DummySizeQuad = 2
) (
input logic sck,
input logic csb,
inout [3:0] sd
// TODO: Add WP
);
// Local parameter : affected by EnFastSim
// Timing information s_ ms_ us
localparam int unsigned tPPDiv = (EnFastSim) ? 1_000 : 1 ;
localparam int unsigned tSEDiv = (EnFastSim) ? 5_000 : 1 ;
localparam int unsigned tBE32Div = (EnFastSim) ? 10_000 : 1 ;
localparam int unsigned tBE64Div = (EnFastSim) ? 100_000 : 1 ;
localparam int unsigned tCEDiv = (EnFastSim) ? 10_000_000 : 1 ;
// Program Latency
localparam int unsigned LocalPPTyp = tPPTyp / tPPDiv; // us
localparam int unsigned LocalPPMax = tPPMax / tPPDiv; // us
// Sector Erase(4kB) Latency
localparam int unsigned LocalSETyp = tSETyp / tSEDiv; // us
localparam int unsigned LocalSEMax = tSEMax / tSEDiv; // us
// Block Erase 32kB Latency
localparam int unsigned LocalBE32Typ = tBE32Typ / tBE32Div; // us
localparam int unsigned LocalBE32Max = tBE32Max / tBE32Div; // us
// Block Erase 64kB Latency
localparam int unsigned LocalBE64Typ = tBE64Typ / tBE64Div; // us
localparam int unsigned LocalBE64Max = tBE64Max / tBE64Div; // us
// Chip Erase Latency
localparam int unsigned LocalCETyp = tCETyp / tCEDiv; // us
localparam int unsigned LocalCEMax = tCEMax / tCEDiv; // us
typedef enum int unsigned {
Addr3B,
Addr4B,
AddrCfg
} addr_mode_e;
// Return Output mode
typedef enum int unsigned {
IoSingle = 1,
IoDual = 2,
IoQuad = 4
} io_mode_e;
typedef logic [7:0] spiflash_byte_t;
localparam spiflash_byte_t Cc = 8'h 7F;
localparam int unsigned StorageAw = $clog2(FlashSize);
// Status Bit Location
typedef enum int unsigned {
StatusBUSY = 0,
StatusWEL = 1,
StatusBP0 = 2,
StatusBP1 = 3,
StatusBP2 = 4,
StatusBP3 = 5,
StatusQE = 6, // Quad Enable
StatusSRWD = 7 // Status Register Write Protect
} status_bit_e;
// spiflash_addr_t is the smallest address width to represent FlashSize bytes
typedef logic [StorageAw-1:0] spiflash_addr_t;
logic [3:0] sd_out, sd_en;
assign sd[0] = sd_en[0] ? sd_out[0] : 1'b z;
assign sd[1] = sd_en[1] ? sd_out[1] : 1'b z;
assign sd[2] = sd_en[2] ? sd_out[2] : 1'b z;
assign sd[3] = sd_en[3] ? sd_out[3] : 1'b z;
// SPI Flash internal state variables
addr_mode_e addr_mode;
spiflash_byte_t [2:0] status; // 24 bit
spiflash_byte_t storage[spiflash_addr_t]; // Associative Array to store data
spiflash_byte_t page_buffer[PageSize]; // page buffer to be used
int unsigned page_buffer_size;
spiflash_byte_t sfdp[256]; // SFDP storage 256Byte
mailbox backend_process; // Start delayed process (after CSb release)
mailbox backend_data; // Deliver the address to the backend task
initial begin
print_banner();
// Init values
status = '0;
backend_process = new(1); // Only one process at a time but multiple data
backend_data = new(1);
// SFDP initialization
// FIXME: Appropriate SFDP table rather than random data
foreach(sfdp[i]) sfdp[i] = $urandom_range(255, 0);
// Should wait ??
fork
main();
backend();
join
end
function automatic void print_banner();
$display("/////////////////////");
$display("// SPI Flash Model //");
$display("/////////////////////");
// Print current config
$display("");
$display("Configurations:");
$display(" JEDEC ID: ID(0x%2X) CC_NUM (%2d)", JedecId, CcNum);
$display(" Capacity: %s", print_capacity(FlashSize));
$display(" Fast Read: %s/ Dummy Cycle (%1d)",
"Enabled", DummySizeNormal);
$display(" Dual Mode:%s/ Dummy Cycle (%1d)",
(EnFastReadDual) ? "Enabled" : "Disabled", DummySizeDual);
$display(" Quad Mode:%s/ Dummy Cycle (%1d)",
(EnFastReadQuad) ? "Enabled" : "Disabled", DummySizeQuad);
endfunction : print_banner
function automatic string print_capacity(int unsigned size);
string str;
// Get the highest unit (GB, MB, kB)
automatic int unsigned quotient;
automatic int unsigned order; // 0: B, 1: kB, 2: MB, 3: GB
automatic string unit;
quotient = size;
forever begin
if (quotient < 1024) break;
quotient = quotient / 1024;
order = order + 1;
end
case (order)
0: unit = "B";
1: unit = "kB";
2: unit = "MB";
3: unit = "GB";
4: unit = "TB";
default: unit = "Unsupported";
endcase
str = $sformatf("%0d%s", quotient, unit);
return str;
endfunction : print_capacity
static task main();
// Main function
forever begin
automatic spiflash_byte_t opcode;
automatic bit early_termination;
automatic int unsigned excessive_sck = 0;
// Indication of process needed after CSb release
automatic bit process_after_csb = 1'b 0;
// Big loop. Wait transaction
sd_en = 4'h 0; // Off the output by default
wait(csb == 1'b 0);
early_termination = 1'b 0;
fork
begin
// If cmd process comes first, this forked process won't set
// early_termination bit
@(posedge csb);
early_termination = 1'b 1;
end
begin
// Main
cmdparse(opcode); // Store incoming into opcod
process_cmd(opcode, process_after_csb);
// Count remaining edges of SCK. opcode completed command should
// have no additional data, or should be discarded
sd_en = 4'h 0;
forever begin
@(posedge sck);
excessive_sck++;
end
end
join_any
// Float the line
sd_en = 4'h 0;
// Determine early termination, if not early terminated, the main block
// waits CSb de-assertion too
// For many Output commands, the logic sends data until host releases
// CSb. In this case, early_termination is set too. If the command is
// output command, do not process early_termination.
// Kill the forked process
disable fork;
// Process program, etc
// If excessive sck, then discard.
if (process_after_csb) begin
if (backend_process.num() != 0) begin
$display("T+%0d] SPIFlash: Can't handle two backend commands", $time);
$display(" Discarding the latter %2Xh", opcode);
continue;
end
backend_process.put(opcode);
end
end
endtask : main
static task backend();
forever begin
automatic spiflash_byte_t opcode;
backend_process.get(opcode);
case (opcode)
spi_device_pkg::CmdPageProgram: begin
backend_page_program();
end
endcase
end
endtask : backend
task automatic cmdparse(
output spiflash_byte_t opcode
);
automatic spiflash_byte_t spi_byte = 0;
get_byte(IoSingle, spi_byte);
$display("SPIFlash: Command(%2Xh) received", spi_byte);
opcode = spi_byte;
endtask : cmdparse
task automatic process_cmd(
input spiflash_byte_t opcode,
output bit en_backend
);
// Main case block to call subtasks depending on the opcode
en_backend = 1'b 0;
case (opcode)
spi_device_pkg::CmdReadStatus1: begin
// Return status data
return_status(0); // 0-2
end
spi_device_pkg::CmdJedecId: begin
return_jedec();
end
spi_device_pkg::CmdReadSfdp: begin
return_sfdp();
end
spi_device_pkg::CmdReadData, spi_device_pkg::CmdReadFast,
spi_device_pkg::CmdReadDual, spi_device_pkg::CmdReadQuad: begin
read(opcode);
end
spi_device_pkg::CmdEn4B, spi_device_pkg::CmdEx4B: begin
addr_4b(opcode);
end
spi_device_pkg::CmdWriteEnable, spi_device_pkg::CmdWriteDisable: begin
wel(opcode);
end
spi_device_pkg::CmdPageProgram: begin
page_program(opcode);
en_backend = 1'b 1;
end
// TODO: SectorErase
// TODO: BlockErase32
// TODO: BlockErase64
default: begin
$display("Unrecognized Opcode (%2Xh) received", opcode);
end
endcase
endtask : process_cmd
task automatic process_cmd_after_csb(
);
endtask : process_cmd_after_csb
task automatic get_byte(
input io_mode_e io_mode,
output spiflash_byte_t data
);
// Receive a byte on SPI
// Expected to be called @(negedge sck);
automatic spiflash_byte_t spi_byte = 0;
// Receive first 8 beats and decoding the byte, call subsequence tasks.
repeat(8/io_mode) begin
@(posedge sck);
case (io_mode)
IoSingle: spi_byte = {spi_byte[6:0], sd[ 0]};
IoDual: spi_byte = {spi_byte[5:0], sd[1:0]};
IoQuad: spi_byte = {spi_byte[3:0], sd[3:0]};
default: spi_byte = {spi_byte[6:0], sd[ 0]};
endcase
end
@(negedge sck); // Wait the data line fully completed
data = spi_byte;
endtask : get_byte
task automatic get_address(
input io_mode_e imode,
input addr_mode_e amode,
output logic [31:0] addr
);
automatic spiflash_byte_t spi_byte;
automatic addr_mode_e mode;
case (amode)
AddrCfg: mode = addr_mode;
Addr3B, Addr4B: mode = amode;
default: mode = Addr3B;
endcase
if (mode == Addr4B) begin
// Get upper byte
get_byte(imode, spi_byte);
addr[31:24] = spi_byte;
end else begin
addr[31:24] = 8'h 0;
end
for(int i = 2 ; i >= 0 ; i--) begin
get_byte(imode, spi_byte);
addr[8*i+:8] = spi_byte;
end
endtask : get_address
task automatic return_byte(spiflash_byte_t data, io_mode_e io_mode);
// Assume the task is called at @(negedge sck);
automatic int unsigned rails;
automatic logic [7:0] lines;
case (io_mode)
IoSingle: begin
rails = 1;
sd_en = 4'b 0010;
end
IoDual: begin
rails = 2;
sd_en = 4'b 0011;
end
IoQuad: begin
rails = 4;
sd_en = 4'b 1111;
end
default: begin
rails = 1;
sd_en = 4'b 0010;
end
endcase
lines = data;
repeat(8/rails) begin
case (io_mode)
IoSingle: begin
sd_out[1] = lines[7];
lines = {lines[6:0], 1'b 0};
end
IoDual: begin
sd_out[1:0] = lines[7:6];
lines = {lines[5:0], 2'b 00};
end
IoQuad: begin
sd_out[3:0] = lines[7:4];
lines = {lines[3:0], 4'b 0000};
end
default: begin
sd_out[1] = lines[7];
lines = {lines[6:0], 1'b 0};
end
endcase
@(negedge sck);
end
endtask : return_byte
task automatic dummy(int unsigned cycle);
// Assume the task is called @(negedge sck);
sd_en = 4'b 0000;
repeat (cycle) @(negedge sck);
endtask : dummy
task automatic return_status(int unsigned idx);
// Starting from the index and wraps %3 until host releases CSb
automatic int unsigned s_idx = idx;
// Copy the status into internal variable to not be corrupted by on-going
// backend tasks.
automatic spiflash_byte_t [2:0] committed_status = status;
forever begin
return_byte(committed_status[s_idx], IoSingle);
s_idx = (s_idx + 1)%3;
end
endtask : return_status
task automatic return_jedec();
// Repeat CcNum then JedecId and DeviceId(MSB to LSB)
repeat (CcNum) begin
return_byte(Cc, IoSingle);
end
return_byte(JedecId, IoSingle);
return_byte(DeviceId[ 7:0], IoSingle);
return_byte(DeviceId[15:8], IoSingle);
// Float
endtask : return_jedec
task automatic return_sfdp();
automatic logic [31:0] address;
// Get address field
get_address(IoSingle, Addr3B, address);
$display("%d] SPIFlash: SFDP Address %6Xh", $time, address);
// dummy byte
dummy(8); // 8 cycle dummy
// fetch data and return
forever begin
return_byte(sfdp[address++], IoSingle);
end
endtask : return_sfdp
task automatic read(
input spiflash_byte_t opcode
);
// Return Read data
automatic logic [31:0] address;
automatic spiflash_addr_t spi_addr;
automatic io_mode_e io_mode = IoSingle;
automatic int unsigned dummy_cycle = 0;
// get address field
get_address(IoSingle, AddrCfg, address);
$display("T:%0d] SPIFlash: Read Address %8Xh", $time, address);
if (address[31:StorageAw] != '0) begin
$display("Out-of-Bound Address received: %8Xh", address);
$display(" Keep returning garbage data");
end
// Dummy cycle?
case (opcode)
spi_device_pkg::CmdReadData: begin
io_mode = IoSingle;
dummy_cycle = 0;
end
spi_device_pkg::CmdReadFast: begin
io_mode = IoSingle;
dummy_cycle = DummySizeNormal;
end
spi_device_pkg::CmdReadDual: begin
io_mode = IoDual;
dummy_cycle = DummySizeDual;
end
spi_device_pkg::CmdReadQuad: begin
io_mode = IoQuad;
dummy_cycle = DummySizeQuad;
end
endcase
if (dummy_cycle != 0) dummy(dummy_cycle);
// fetch from storage and return
while (address <= FlashSize) begin
// Check if exists, if not fill with random data
if (!storage.exists(address)) storage[address] = $urandom_range(255,0);
return_byte(storage[address++], io_mode);
end
endtask : read
task automatic addr_4b(spiflash_byte_t opcode);
// Set / clear addr_mode
case (opcode)
spi_device_pkg::CmdEn4B: addr_mode = Addr4B;
spi_device_pkg::CmdEx4B: addr_mode = Addr3B;
default: $display("addr_4b: Unrecognized Cmd (%2Xh)", opcode);
endcase
endtask : addr_4b
task automatic wel(spiflash_byte_t opcode);
// TODO: Set only when CSb is deasserted right after 8th beat
case (opcode)
spi_device_pkg::CmdWriteEnable: status[0][StatusWEL] = 1'b 1;
spi_device_pkg::CmdWriteDisable: status[0][StatusWEL] = 1'b 0;
default: $display("WEL: Unrecognized Cmd (%2Xh)", opcode);
endcase
endtask : wel
task automatic page_program(spiflash_byte_t opcode);
// Return Read data
automatic logic [31:0] address;
automatic logic [7:0] buffer_idx;
// Set BUSY
status[0][StatusBUSY] = 1'b 1;
// get address field
get_address(IoSingle, AddrCfg, address);
$display("T:%0d] SPIFlash: Program Address %8Xh", $time, address);
if (address[31:StorageAw] != '0) begin
$display("Out-of-Bound Address received");
$display("Discarding the contents");
return;
end
backend_data.put({opcode, address});
// Starting with buffer offset and wraps 256B always
buffer_idx = address[7:0];
page_buffer_size = 0;
// Stack bytes
forever begin
automatic spiflash_byte_t data;
get_byte(IoSingle, data);
page_buffer[buffer_idx % PageSize] = data;
buffer_idx = (buffer_idx + 1 ) % PageSize;
if (page_buffer_size != PageSize) page_buffer_size++;
end
endtask : page_program
task automatic backend_page_program();
automatic spiflash_addr_t spi_addr;
automatic spiflash_byte_t opcode;
automatic logic [39:0] mbx_data;
automatic logic [31:0] host_address;
automatic logic [7:0] buffer_offset;
if (backend_data.num() == 0) begin
// The transaction got cancelled before receiving the address.
// Skip the backend task
status[0][StatusBUSY] = 1'b 0;
return;
end
// Fetch address from mailbox
backend_data.get(mbx_data);
opcode = mbx_data[39:32];
host_address = mbx_data[31:0];
// if opcode is not PageProgram, somehow mailbox corrupted. Report error and put it back to mailbox
if (opcode != spi_device_pkg::CmdPageProgram) begin
$display("SPIFlash: Opcode is not PageProgram, putting the data back to backend_data");
backend_data.put({opcode, host_address});
return;
end
// Write data into storage
buffer_offset = host_address[7:0];
repeat (page_buffer_size) begin
storage[{host_address[31:8], buffer_offset%PageSize}] = page_buffer[buffer_offset%256];
buffer_offset = (buffer_offset+1) % PageSize;
end
// Determines delay
// Didn't use $dist_normal() as Typ is most likely lower bound.
// Maybe Poisson Distribution is appropriate?
#($urandom_range(LocalPPTyp*1.1, LocalPPTyp*0.9) * 1us)
// At the end, clear BUSY
status[0][StatusBUSY] = 1'b 0;
endtask : backend_page_program
initial begin
assert(PageSize == 256)
else begin
$display("SPIFlash currently supports 256B Page only");
$finish();
end
end
endprogram : spiflash