Skip to content

Commit

Permalink
Update README.md
Browse files Browse the repository at this point in the history
  • Loading branch information
JitheshVijay authored May 29, 2024
1 parent 7a0b2e9 commit 004bb21
Showing 1 changed file with 373 additions and 6 deletions.
379 changes: 373 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,375 @@
# 25 Series Flash QSPI Cocotb
The Series 25 flash memory devices typically use the Quad Serial Peripheral Interface (QSPI) for communication. QSPI is an extension of the SPI protocol and allows for data to be transferred four bits at a time, enabling faster read and write speeds compared to standard SPI.
# SPI Interface for Cocotb

# API for QSPI Flash IP
Created a Python class that acts as an API for interacting with the QSPI flash memory module. This class will provide methods for basic operations like reading, writing, and erasing data from the flash memory.
SPI simulation framework for cocotb.

# Test Cases for Verification
Wrote test cases to verify the functionality of the QSPI flash memory module using the API we defined.
## Documentation
To properly handle QSPI (Quad SPI), which involves four data lines (IO0, IO1, IO2, IO3), we need to update the module to include these lines and adjust the communication logic accordingly. Below is the revised Verilog module and testbench documentation to support QSPI with four data lines.

### Creating a custom cocotbext.qspi
Directory Structure of QSPI module:

```markdown
cocotbext/
__init__.py
qspi/
__init__.py
qspi_bus.py
qspi_master.py
qspi_config.py
```

cocotbext/__init__.py
```
# This file is empty
```
cocotbext/qspi/__init__.py

```
from .qspi_bus import QspiBus
from .qspi_master import QspiMaster
from .qspi_config import QspiConfig
__all__ = ["QspiBus", "QspiMaster", "QspiConfig"]
```
##### QspiBus
This class will handle the initialization of the QSPI signals.

qspi_bus.py
```python
import cocotb
from cocotb.handle import SimHandle

class QspiBus:
def __init__(self, sclk, cs, io0, io1, io2, io3):
self.sclk = sclk # Serial clock signal
self.cs = cs # Chip select signal
self.io0 = io0 # Data line 0
self.io1 = io1 # Data line 1
self.io2 = io2 # Data line 2
self.io3 = io3 # Data line 3

@classmethod
def from_prefix(cls, entity: SimHandle, prefix: str):
# Retrieve signals based on prefix
sclk = getattr(entity, f"{prefix}_sclk")
cs = getattr(entity, f"{prefix}_cs")
io0 = getattr(entity, f"{prefix}_io0")
io1 = getattr(entity, f"{prefix}_io1")
io2 = getattr(entity, f"{prefix}_io2")
io3 = getattr(entity, f"{prefix}_io3")
return cls(sclk, cs, io0, io1, io2, io3)

```
##### QspiConfig
This class will handle the QSPI configuration parameters.

qspi_config.py
```python
class QspiConfig:
def __init__(self, word_width, sclk_freq, cpol, cpha, cs_active_low, quad_mode=True):
self.word_width = word_width # Data width in bits
self.sclk_freq = sclk_freq # Serial clock frequency
self.cpol = cpol # Clock polarity
self.cpha = cpha # Clock phase
self.cs_active_low = cs_active_low # Chip select active low flag
self.quad_mode = quad_mode # Quad mode flag (default True)
```
##### QspiMaster
This class will handle the QSPI operations.

qspi_master.py
```python
import cocotb
from cocotb.triggers import Timer, RisingEdge

class QspiMaster:
def __init__(self, bus: QspiBus, config: QspiConfig):
self.bus = bus # QSPI bus signals
self.config = config # QSPI configuration

async def write(self, data):
await self._start_transaction() # Start the transaction
for byte in data:
await self._write_byte(byte) # Write each byte
await self._end_transaction() # End the transaction

async def read(self, length):
await self._start_transaction() # Start the transaction
data = [await self._read_byte() for _ in range(length)] # Read bytes
await self._end_transaction() # End the transaction
return data # Return the read data

async def _start_transaction(self):
self.bus.cs.value = 0 if self.config.cs_active_low else 1 # Assert chip select
await Timer(1, units='ns') # Wait for 1 ns

async def _end_transaction(self):
self.bus.cs.value = 1 if self.config.cs_active_low else 0 # Deassert chip select
await Timer(1, units='ns') # Wait for 1 ns

async def _write_byte(self, byte):
for i in range(8):
self.bus.io0.value = (byte >> (7 - i)) & 1 # Set data line
await RisingEdge(self.bus.sclk) # Wait for clock edge
await Timer(1, units='ns') # Wait for 1 ns

async def _read_byte(self):
byte = 0
for i in range(8):
await RisingEdge(self.bus.sclk) # Wait for clock edge
byte = (byte << 1) | int(self.bus.io0.value) # Read data line
return byte # Return the read byte

```
### QSPI Flash Memory Interface Documentation

#### Overview
This document provides a detailed explanation of the QSPI Flash Memory Interface, implemented in Verilog, and its verification using Cocotb. The interface allows for read, write, and erase operations on the QSPI flash memory using four data lines (IO0, IO1, IO2, IO3).

### Interface Description
The QSPI (Quad Serial Peripheral Interface) is designed for communication between a master (e.g., a microcontroller) and a slave (e.g., flash memory). The interface includes the following signals:

| Signal Name | Direction | Description |
|---------------|-----------|--------------------------------------------------|
| `qspi_sclk` | Input | QSPI serial clock input |
| `qspi_cs` | Input | QSPI chip select input |
| `qspi_io0` | Inout | QSPI data line 0 (MOSI in single SPI mode) |
| `qspi_io1` | Inout | QSPI data line 1 (MISO in single SPI mode) |
| `qspi_io2` | Inout | QSPI data line 2 |
| `qspi_io3` | Inout | QSPI data line 3 |
| `reset_n` | Input | Active-low reset input |
| `clk` | Input | Clock input for internal logic |
| `write_enable`| Input | Write enable signal |
| `read_enable` | Input | Read enable signal |
| `erase_enable`| Input | Erase enable signal |
| `data_in` | Input | Data input for write operation |
| `address` | Input | Address input for memory access |
| `data_out` | Output | Data output for read operation |

### Verilog Module
The Verilog module `qspi_flash` is responsible for handling the memory operations based on the given signals. Below is the Verilog code for the QSPI Flash Memory module:

```verilog
module qspi_flash (
input wire qspi_sclk, // QSPI serial clock input
input wire qspi_cs, // QSPI chip select input
inout wire qspi_io0, // QSPI data line 0 (MOSI in single SPI mode)
inout wire qspi_io1, // QSPI data line 1 (MISO in single SPI mode)
inout wire qspi_io2, // QSPI data line 2
inout wire qspi_io3, // QSPI data line 3
input wire reset_n, // Active-low reset input
input wire clk, // Clock input for internal logic
input wire write_enable, // Write enable signal
input wire read_enable, // Read enable signal
input wire erase_enable, // Erase enable signal
input wire [7:0] data_in, // Data input for write operation
input wire [7:0] address, // Address input for memory access
output reg [7:0] data_out // Data output for read operation
);
reg [7:0] memory [0:255]; // Memory array to store data, 256 bytes in size
reg [7:0] data_buffer; // Buffer to handle bidirectional data lines
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
data_out <= 8'hFF; // Initialize data_out to 0xFF on reset
end else begin
if (write_enable) begin
memory[address] <= data_in; // Write data_in to memory at specified address
end
if (read_enable) begin
data_out <= memory[address]; // Read data from memory at specified address
end
if (erase_enable) begin
memory[address] <= 8'hFF; // Erase data by writing 0xFF to memory at specified address
end
end
end
// Assign QSPI data lines based on operation mode
assign qspi_io0 = (write_enable || erase_enable) ? data_in[0] : 1'bz;
assign qspi_io1 = (write_enable || erase_enable) ? data_in[1] : 1'bz;
assign qspi_io2 = (write_enable || erase_enable) ? data_in[2] : 1'bz;
assign qspi_io3 = (write_enable || erase_enable) ? data_in[3] : 1'bz;
endmodule
```

### Cocotb Testbench
The following Python code provides a Cocotb testbench for verifying the QSPI Flash Memory module. The testbench includes methods for initializing the device, writing to memory, reading from memory, and erasing memory.

#### QSPIFlash Class
The `QSPIFlash` class encapsulates the functionality needed to interact with the QSPI flash memory.

```python
import cocotb
from cocotb.triggers import Timer
from cocotb.clock import Clock
from cocotbext.qspi import QspiBus, QspiMaster, QspiConfig

class QSPIFlash:
def __init__(self, dut):
# Initialize QSPIFlash class with the DUT (Device Under Test)
self.dut = dut

# Initialize QSPI bus using the prefix 'qspi' from the DUT signals
self.qspi_bus = QspiBus.from_prefix(dut, "qspi")

# Configure QSPI communication parameters
self.qspi_config = QspiConfig(
word_width=8,
sclk_freq=25e6, # SCLK frequency of 25 MHz
cpol=False, # Clock polarity (CPOL) = 0
cpha=False, # Clock phase (CPHA) = 0
msb_first=True, # Most significant bit first
cs_active_low=True # Chip select is active low
)

# Initialize QSPI master with the configured bus and settings
self.qspi_master = QspiMaster(self.qspi_bus, self.qspi_config)

async def reset(self):
# Reset the device by toggling the reset_n signal
self.dut.reset_n.value = 0
await Timer(100, units='ns') # Wait for 100 ns
self.dut.reset_n.value = 1
await Timer(100, units='ns') # Wait for another 100 ns

async def initialize(self):
# Start the clock with a period of 10 ns
cocotb.start_soon(Clock(self.dut.clk, 10, units='ns').start())

# Call the reset method to initialize the device
await self.reset()

async def write(self, address, data):
# Send a write command along with address and data
command = [0x02] # Write command
address_bytes = [(address >> i) & 0xFF for i in (16, 8, 0)] # Split address into bytes
data_bytes = [data] # Convert data to bytes
await self.qspi_master.write(command + address_bytes + data_bytes) # Send command, address, and data
await self.qspi_master.wait() # Wait for QSPI transaction to complete

async def read(self, address):
# Send a read command along with address to read data
command = [0x03] # Read command
address_bytes = [(address >> i) & 0xFF for i in (16, 8, 0)] # Split address into bytes
await self.qspi_master.write(command + address_bytes) # Send command and address
await self.qspi_master.wait() # Wait for QSPI transaction to complete

# Read one byte of data from QSPI
read_data = await self.qspi_master.read(1)

# Handle high-impedance state by resolving to 0xFF
read_data_resolved = int(read_data[0].value) if read_data[0].value.is_resolvable else 0xFF

return read_data_resolved

async def erase(self, address):
# Send a sector erase command along with address
command = [0x20] # Sector erase command
address_bytes = [(address >> i) & 0xFF for i in (16, 8, 0)] # Split address into bytes
await self.qspi_master.write(command + address_bytes) # Send command and address
await self.qspi_master.wait() # Wait for QSPI transaction to complete

```

#### Test Cases
Two test cases are provided to verify the write/read and erase functionalities of the QSPI flash memory.

##### Test Case: Write and Read
```python
@cocotb.test()
async def test_qspi_flash_write_read(dut):
flash = QSPIFlash(dut)
await flash.initialize()

address = 0x00
data_to_write = 0xA5
await flash.write(address, data_to_write)
await Timer(10, units='ns')

read_data = await flash.read(address)
assert read_data == data_to_write, f"Data mismatch: {read_data} != {data_to_write}"
```

##### Test Case: Erase
```python
@cocotb.test()
async def test_qspi_flash_erase(dut):
flash = QSPIFlash(dut)
await flash.initialize()

address = 0x00
data_to_write = 0x5A
await flash.write(address, data_to_write)
await Timer(10, units='ns')

await flash.erase(address)
await Timer(10, units='ns')

read_data = await flash.read(address)
assert read_data == 0xFF, f"Data after erase mismatch: {read_data} != 0xFF"
```

### QSPI Flash Test Module
The Verilog test module `qspi_flash_test` sets up the testbench environment to simulate the QSPI flash memory interface.

```verilog
module qspi_flash_test;
reg qspi_sclk; // QSPI serial clock
reg qspi_mosi; // QSPI master output, slave input
wire qspi_miso; // QSPI master input, slave output
reg qspi_cs; // QSPI chip select
reg reset_n; // Active-low reset signal
reg clk; // Clock signal
reg write_enable; // Write enable signal
reg read_enable; // Read enable signal
reg erase_enable; // Erase enable signal
reg [7:0] data_in
; // Data input for memory operations
reg [7:0] address; // Address input for memory operations
wire [7:0] data_out; // Data output from memory operations
qspi_flash dut (
.qspi_sclk(qspi_sclk),
.qspi_cs(qspi_cs),
.qspi_io0(qspi_io0),
.qspi_io1(qspi_io1),
.qspi_io2(qspi_io2),
.qspi_io3(qspi_io3),
.reset_n(reset_n),
.clk(clk),
.write_enable(write_enable),
.read_enable(read_enable),
.erase_enable(erase_enable),
.data_in(data_in),
.address(address),
.data_out(data_out)
);
initial begin
$dumpfile("waves.vcd");
$dumpvars(0, qspi_flash_test);
qspi_sclk = 0;
qspi_mosi = 0;
qspi_cs = 1;
reset_n = 0;
clk = 0;
write_enable = 0;
read_enable = 0;
erase_enable = 0;
data_in = 0;
address = 0;
#10 reset_n = 1;
end
always #5 clk = ~clk;
endmodule
```

### Conclusion
This documentation provides a comprehensive guide to implementing and simulating a QSPI flash memory interface using Verilog and Cocotb. The QSPI interface supports essential memory operations such as read, write, and erase, and the provided testbench ensures these functionalities are correctly verified. Adjust and expand the simulation code as necessary to meet your specific requirements.

0 comments on commit 004bb21

Please sign in to comment.