-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7a0b2e9
commit 004bb21
Showing
1 changed file
with
373 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |