feat(spi2tlul): Implement efficient bulk data transfers Implements a high-throughput bulk data transfer mechanism for the Spi2TLUL bridge. The previous implementation relied on single-byte read/write operations to the data buffer port, which was inefficient for large data transfers due to the overhead of sending an address for every byte. This change introduces dedicated bulk read and write ports. A single SPI command can now initiate a transfer of up to 256 bytes, significantly improving data throughput for tasks like loading programs or model weights. Key changes include: - A new command-decoding FSM in the Spi2TLUL Chisel module to distinguish between single-register access and bulk transfers. - A robust, double-buffered MISO path to prevent data loss during back-to-back SPI transactions. - New `new_bulk_write` and `new_bulk_read` methods in the Python `SPIMaster` test utility. - Enumerations for SPI register addresses and commands in the Python test utilities for improved readability and maintainability. - Comprehensive cocotb tests verifying the new bulk transfer modes, including a stress test for transfers up to 64KB. Change-Id: I72c614fadb5620c86a4150bedffef14857f47217
diff --git a/hdl/chisel/src/bus/Spi2TLUL.scala b/hdl/chisel/src/bus/Spi2TLUL.scala index a134f93..8cb6027 100644 --- a/hdl/chisel/src/bus/Spi2TLUL.scala +++ b/hdl/chisel/src/bus/Spi2TLUL.scala
@@ -43,18 +43,21 @@ // Reset is active when csb is high (inactive) OR when the main reset is active. val combined_reset = io.spi.csb || spi_domain_reset.asBool - val (mosi_data_reg, miso_data_reg, miso_valid_reg, bit_count_reg, deq_attempted_reg) = + val (mosi_data_reg, bit_count_reg) = withClockAndReset(io.spi.clk, combined_reset.asAsyncReset) { val mosi = RegInit(0.U(8.W)) - val miso = RegInit(0.U(8.W)) - // Gate MISO output until the first SPI clock cycle to prevent X propagation. - val miso_valid = RegInit(false.B) val bit_count = RegInit(0.U(3.W)) - val deq_attempted = RegInit(false.B) - (mosi, miso, miso_valid, bit_count, deq_attempted) + (mosi, bit_count) } - miso_valid_reg := miso_valid_reg || !io.spi.csb - io.spi.miso := Mux(miso_valid_reg, miso_data_reg(7), 0.U) + + val miso_buffer_reg = withClockAndReset(io.spi.clk, spi_domain_reset.asAsyncReset) { + RegInit(0.U.asTypeOf(Valid(UInt(8.W)))) + } + val next_miso_byte_reg = withClockAndReset(io.spi.clk, spi_domain_reset.asAsyncReset) { + RegInit(0.U.asTypeOf(Valid(UInt(8.W)))) + } + + io.spi.miso := Mux(miso_buffer_reg.valid, miso_buffer_reg.bits(7), 0.U) val spi2tlul_q = Module(new AsyncQueue(UInt(8.W), AsyncQueueParams(depth = 2, safe = false))) spi2tlul_q.io.enq_clock := io.spi.clk @@ -63,12 +66,50 @@ spi2tlul_q.io.deq_reset := reset.asBool val completed_byte = Cat(mosi_data_reg(6,0), io.spi.mosi) - spi2tlul_q.io.enq.valid := bit_count_reg === 7.U + val byte_received = bit_count_reg === 7.U + + val spi_bulk_read_len_reg = withClockAndReset(io.spi.clk, spi_domain_reset.asAsyncReset) { + RegInit(0.U(8.W)) + } + val spi_bulk_read_sent_count_reg = withClockAndReset(io.spi.clk, spi_domain_reset.asAsyncReset) { + RegInit(0.U(8.W)) + } + + object SpiCmdState extends ChiselEnum { + val sIdle, sGotBulkReadAddr, sSendData, sGotOtherCmd = Value + } + val spi_cmd_state = withClock(io.spi.clk) { RegInit(SpiCmdState.sIdle) } + + val is_first_byte_reg = withClockAndReset(io.spi.clk, io.spi.csb.asAsyncReset) { + RegInit(true.B) + } + is_first_byte_reg := Mux(byte_received, false.B, is_first_byte_reg) + val is_first_byte = is_first_byte_reg && byte_received + + val is_write_cmd = completed_byte(7) + val cmd_addr = completed_byte(6,0) + val is_truly_a_bulk_read_cmd = (spi_cmd_state === SpiCmdState.sIdle) && is_first_byte && is_write_cmd && (cmd_addr === SpiRegAddress.BULK_READ_PORT.asUInt) + + val next_spi_cmd_state = MuxCase(spi_cmd_state, Seq( + (spi_cmd_state === SpiCmdState.sIdle && is_truly_a_bulk_read_cmd) -> SpiCmdState.sGotBulkReadAddr, + (spi_cmd_state === SpiCmdState.sIdle && !is_truly_a_bulk_read_cmd && is_first_byte && is_write_cmd) -> SpiCmdState.sGotOtherCmd, + (spi_cmd_state === SpiCmdState.sGotBulkReadAddr) -> SpiCmdState.sSendData, + (spi_cmd_state === SpiCmdState.sSendData && spi_bulk_read_sent_count_reg === spi_bulk_read_len_reg) -> SpiCmdState.sIdle, + (spi_cmd_state === SpiCmdState.sGotOtherCmd) -> SpiCmdState.sIdle, + )) + spi_cmd_state := Mux(byte_received, next_spi_cmd_state, spi_cmd_state) + + val is_bulk_read_start = byte_received && (spi_cmd_state === SpiCmdState.sGotBulkReadAddr) + val block_cmd_enqueue = (spi_cmd_state === SpiCmdState.sGotBulkReadAddr) || (is_truly_a_bulk_read_cmd && byte_received) + + val is_bulk_status_read = is_first_byte && !completed_byte(7) && (completed_byte(6,0) === SpiRegAddress.BULK_READ_STATUS_REG.asUInt) && (spi_cmd_state =/= SpiCmdState.sGotOtherCmd) + + spi2tlul_q.io.enq.valid := byte_received && !is_bulk_status_read && !block_cmd_enqueue spi2tlul_q.io.enq.bits := completed_byte dontTouch(spi2tlul_q.io.enq) object SpiState extends ChiselEnum { - val sIDLE, sWAIT_WRITE_DATA, sSEND_READ_DATA = Value + val sIDLE, sWAIT_WRITE_DATA, sSEND_READ_DATA, sBULK_WRITE_DATA, sBULK_READ_DATA = Value } val spi_state_reg = RegInit(SpiState.sIDLE) @@ -83,16 +124,34 @@ val TL_STATUS_REG = 0x06.U val DATA_BUF_PORT = 0x07.U val TL_WRITE_STATUS_REG = 0x08.U + val BULK_WRITE_PORT = 0x09.U + val BULK_READ_PORT = 0x0A.U + val BULK_READ_STATUS_REG = 0x0B.U } // Physical registers backing the map val tl_addr_reg = RegInit(VecInit(Seq.fill(4)(0.U(8.W)))) val tl_len_reg = RegInit(0.U(8.W)) + val bulk_len_reg = RegInit(0.U(8.W)) + val bulk_count_reg = RegInit(0.U(8.W)) // Command and Status registers are handled by the TL FSM, not stored directly here. - val data_buffer = RegInit(VecInit(Seq.fill(16)(0.U(128.W)))) - val bulk_read_ptr = RegInit(0.U(8.W)) // Byte pointer into the data buffer + val write_data_buffer = RegInit(VecInit(Seq.fill(16)(0.U(128.W)))) + val read_data_buffer = withClockAndReset(io.spi.clk, spi_domain_reset.asAsyncReset) { + RegInit(VecInit(Seq.fill(16)(0.U(128.W)))) + } + val bulk_read_write_ptr = withClockAndReset(io.spi.clk, spi_domain_reset.asAsyncReset) { + RegInit(0.U(4.W)) + } + val spi_bulk_read_ptr = withClockAndReset(io.spi.clk, spi_domain_reset.asAsyncReset) { + RegInit(0.U(8.W)) // Byte pointer into the data buffer + } val bulk_write_ptr = RegInit(0.U(8.W)) // Byte pointer for writes + val bytes_written = Cat(bulk_read_write_ptr, 0.U(4.W)) + val bytes_read = spi_bulk_read_ptr + val bulk_read_bytes_available = Wire(UInt(9.W)) + bulk_read_bytes_available := bytes_written - bytes_read + val addr_reg = RegInit(0.U(7.W)) // === TileLink Read FSM === @@ -122,11 +181,11 @@ val tl_cmd_reg_write = do_write && (addr_reg === SpiRegAddress.TL_CMD_REG.asUInt) val tl_cmd_reg_data = spi2tlul_q.io.deq.bits - val tlul2spi_q = Module(new AsyncQueue(UInt(8.W), AsyncQueueParams.singleton(safe = false))) - tlul2spi_q.io.enq_clock := clock - tlul2spi_q.io.enq_reset := reset.asBool - tlul2spi_q.io.deq_clock := io.spi.clk - tlul2spi_q.io.deq_reset := reset.asBool + val tl_to_spi_bulk_q = Module(new AsyncQueue(UInt(128.W), AsyncQueueParams(depth = 2, safe = false))) + tl_to_spi_bulk_q.io.enq_clock := clock + tl_to_spi_bulk_q.io.enq_reset := reset.asBool + tl_to_spi_bulk_q.io.deq_clock := io.spi.clk + tl_to_spi_bulk_q.io.deq_reset := spi_domain_reset.asBool // Add queues for TileLink channels to handle backpressure val tl_a_q = Module(new Queue(new OpenTitanTileLink.A_Channel(tlul_p), 1)) @@ -134,21 +193,43 @@ io.tl.a <> tl_a_q.io.deq io.tl.a.bits := RequestIntegrityGen(tlul_p, tl_a_q.io.deq.bits) tl_d_q.io.enq <> io.tl.d - tlul2spi_q.io.deq.ready := !io.spi.csb && !deq_attempted_reg + + val tlul2spi_q = Module(new AsyncQueue(UInt(8.W), AsyncQueueParams.singleton(safe = false))) + tlul2spi_q.io.enq_clock := clock + tlul2spi_q.io.enq_reset := reset.asBool + tlul2spi_q.io.deq_clock := io.spi.clk + tlul2spi_q.io.deq_reset := reset.asBool + + // Connect TL D-channel to the bulk queue enqueue port + tl_to_spi_bulk_q.io.enq.valid := tl_d_q.io.deq.valid && (tl_read_state_reg === TlReadState.sWaitBeatAck) + tl_to_spi_bulk_q.io.enq.bits := tl_d_q.io.deq.bits.data + + tlul2spi_q.io.deq.ready := !next_miso_byte_reg.valid && !io.spi.csb && (spi_cmd_state =/= SpiCmdState.sSendData) + + // Always be ready to receive read data from the TL domain + tl_to_spi_bulk_q.io.deq.ready := true.B // FSM logic val deq_ready = spi_state_reg === SpiState.sIDLE || - spi_state_reg === SpiState.sWAIT_WRITE_DATA + spi_state_reg === SpiState.sWAIT_WRITE_DATA || + spi_state_reg === SpiState.sBULK_WRITE_DATA spi2tlul_q.io.deq.ready := deq_ready - tlul2spi_q.io.enq.valid := (spi_state_reg === SpiState.sSEND_READ_DATA) + tlul2spi_q.io.enq.valid := (spi_state_reg === SpiState.sSEND_READ_DATA) || + (spi_state_reg === SpiState.sBULK_READ_DATA) val is_write = spi2tlul_q.io.deq.bits(7) val state_next = MuxCase(spi_state_reg, Seq( (spi_state_reg === SpiState.sIDLE && spi2tlul_q.io.deq.fire) -> Mux(is_write, SpiState.sWAIT_WRITE_DATA, SpiState.sSEND_READ_DATA), (spi_state_reg === SpiState.sWAIT_WRITE_DATA && spi2tlul_q.io.deq.fire) -> - SpiState.sIDLE, + Mux(addr_reg === SpiRegAddress.BULK_WRITE_PORT.asUInt, SpiState.sBULK_WRITE_DATA, + Mux(addr_reg === SpiRegAddress.BULK_READ_PORT.asUInt, SpiState.sBULK_READ_DATA, + SpiState.sIDLE)), (spi_state_reg === SpiState.sSEND_READ_DATA && tlul2spi_q.io.enq.fire) -> + SpiState.sIDLE, + (spi_state_reg === SpiState.sBULK_WRITE_DATA && spi2tlul_q.io.deq.fire && ((bulk_count_reg +& 1.U) === (bulk_len_reg +& 1.U))) -> + SpiState.sIDLE, + (spi_state_reg === SpiState.sBULK_READ_DATA && tlul2spi_q.io.enq.fire && ((bulk_count_reg +& 1.U) === (bulk_len_reg +& 1.U))) -> SpiState.sIDLE )) spi_state_reg := state_next @@ -169,21 +250,28 @@ val writing_len_reg = do_write && addr_reg === SpiRegAddress.TL_LEN_REG.asUInt tl_len_reg := Mux(writing_len_reg, data, tl_len_reg) + val writing_bulk_write_port = do_write && addr_reg === SpiRegAddress.BULK_WRITE_PORT.asUInt + val writing_bulk_read_port = do_write && addr_reg === SpiRegAddress.BULK_READ_PORT.asUInt + bulk_len_reg := Mux(writing_bulk_write_port || writing_bulk_read_port, data, bulk_len_reg) + val write_word_index = bulk_write_ptr(7,4) val write_byte_index = bulk_write_ptr(3,0) val write_shift = write_byte_index << 3 val write_mask = ~(0xFF.U << write_shift) - val write_old_word = data_buffer(write_word_index) + val write_old_word = write_data_buffer(write_word_index) val write_new_word = (write_old_word & write_mask) | (data << write_shift) val write_cmd_fire = tl_cmd_reg_write && tl_cmd_reg_data === 2.U - val writing_data_buf = do_write && addr_reg === SpiRegAddress.DATA_BUF_PORT.asUInt - bulk_write_ptr := Mux(write_cmd_fire, 0.U, + val writing_data_buf_single = do_write && addr_reg === SpiRegAddress.DATA_BUF_PORT.asUInt + val writing_bulk_data = spi_state_reg === SpiState.sBULK_WRITE_DATA && spi2tlul_q.io.deq.fire + val writing_data_buf = writing_data_buf_single || writing_bulk_data + val start_bulk_write = do_write && addr_reg === SpiRegAddress.BULK_WRITE_PORT.asUInt + bulk_write_ptr := Mux(write_cmd_fire || start_bulk_write, 0.U, Mux(writing_data_buf, bulk_write_ptr + 1.U, bulk_write_ptr)) // sSEND_READ_DATA - val word_index = bulk_read_ptr(7,4) - val byte_index = bulk_read_ptr(3,0) - val selected_word = data_buffer(word_index) + val word_index = spi_bulk_read_ptr(7,4) + val byte_index = spi_bulk_read_ptr(3,0) + val selected_word = write_data_buffer(word_index) val status_map = Seq( TlReadState.sIdle.asUInt -> 0x00.U, @@ -211,26 +299,74 @@ SpiRegAddress.TL_WRITE_STATUS_REG.asUInt -> MuxLookup(tl_write_state_reg.asUInt, 0.U)(write_status_map), SpiRegAddress.DATA_BUF_PORT.asUInt -> (selected_word.asUInt >> (byte_index << 3.U))(7,0), + SpiRegAddress.BULK_READ_PORT.asUInt -> (selected_word.asUInt >> (byte_index << 3.U))(7,0), ) tlul2spi_q.io.enq.bits := MuxLookup(addr_reg, 0.U(8.W))(read_map) val read_cmd_fire = tl_cmd_reg_write && tl_cmd_reg_data === 1.U - val reading_data_buf = spi_state_reg === SpiState.sSEND_READ_DATA && - tlul2spi_q.io.enq.fire && - addr_reg === SpiRegAddress.DATA_BUF_PORT.asUInt - bulk_read_ptr := Mux(read_cmd_fire, 0.U, - Mux(reading_data_buf, bulk_read_ptr + 1.U, bulk_read_ptr)) + val reading_bulk_data = spi_state_reg === SpiState.sBULK_READ_DATA && tlul2spi_q.io.enq.fire + val start_bulk_read = do_write && addr_reg === SpiRegAddress.BULK_READ_PORT.asUInt + + bulk_count_reg := Mux(start_bulk_write || start_bulk_read, 0.U, + Mux(writing_bulk_data || reading_bulk_data, bulk_count_reg + 1.U, bulk_count_reg)) withClock(io.spi.clk) { + // Combinational signal for decrementing byte counter + val reading_bulk_data_byte = spi_cmd_state === SpiCmdState.sSendData && byte_received + + read_data_buffer(bulk_read_write_ptr) := Mux(tl_to_spi_bulk_q.io.deq.fire, tl_to_spi_bulk_q.io.deq.bits, read_data_buffer(bulk_read_write_ptr)) + bulk_read_write_ptr := Mux(tl_to_spi_bulk_q.io.deq.fire, bulk_read_write_ptr + 1.U, bulk_read_write_ptr) + + spi_bulk_read_ptr := Mux(reading_bulk_data_byte, spi_bulk_read_ptr + 1.U, spi_bulk_read_ptr) + + val reset_sent_count = spi_cmd_state === SpiCmdState.sGotBulkReadAddr && byte_received + spi_bulk_read_sent_count_reg := Mux(reset_sent_count, 0.U, + Mux(reading_bulk_data_byte, spi_bulk_read_sent_count_reg + 1.U, spi_bulk_read_sent_count_reg)) + + spi_bulk_read_len_reg := Mux(is_bulk_read_start, completed_byte, spi_bulk_read_len_reg) + mosi_data_reg := Cat(mosi_data_reg(6,0), io.spi.mosi) bit_count_reg := bit_count_reg + 1.U - deq_attempted_reg := Mux(bit_count_reg === 0.U, true.B, deq_attempted_reg) + val read_word_index = spi_bulk_read_ptr(7,4) + val read_byte_index = spi_bulk_read_ptr(3,0) + val selected_read_word = read_data_buffer(read_word_index) + val selected_read_byte = (selected_read_word >> (read_byte_index << 3.U))(7,0) - miso_data_reg := MuxCase(miso_data_reg, Seq( - (bit_count_reg === 0.U && tlul2spi_q.io.deq.fire) -> tlul2spi_q.io.deq.bits, - (bit_count_reg =/= 0.U) -> Cat(miso_data_reg(6,0), 0.U(1.W)), + // --- MISO Path Refactor with Forwarding --- + + // 1. Define the single source of new data and its validity + val miso_data_source_bits = MuxCase(0.U, Seq( + (is_bulk_read_start || reading_bulk_data_byte) -> selected_read_byte, + is_bulk_status_read -> bulk_read_bytes_available, + tlul2spi_q.io.deq.fire -> tlul2spi_q.io.deq.bits )) + val miso_data_source_valid = is_bulk_read_start || reading_bulk_data_byte || is_bulk_status_read || tlul2spi_q.io.deq.fire + + // 2. Define the key conditions + val load_shifter = bit_count_reg === 0.U + + // 3. Logic for the shifter register (miso_buffer_reg) + // It loads at the start of a byte. It must take new data if available (forwarding), + // otherwise it takes data from the staging register. + val shifter_valid_source = Mux(miso_data_source_valid, true.B, next_miso_byte_reg.valid) + val shifter_bits_source = Mux(miso_data_source_valid, miso_data_source_bits, next_miso_byte_reg.bits) + + val miso_buffer_reg_valid_next = Mux(load_shifter, shifter_valid_source, miso_buffer_reg.valid) + val miso_buffer_reg_bits_next = Mux(load_shifter, shifter_bits_source, Cat(miso_buffer_reg.bits(6,0), 0.U)) + + // 4. Logic for the staging register (next_miso_byte_reg) + // It is cleared ONLY if the shifter consumes its data AND no new data arrives to replace it. + val stage_is_consumed = load_shifter && !miso_data_source_valid + val next_miso_byte_reg_valid_next = Mux(miso_data_source_valid, true.B, + Mux(stage_is_consumed, false.B, next_miso_byte_reg.valid)) + val next_miso_byte_reg_bits_next = Mux(miso_data_source_valid, miso_data_source_bits, next_miso_byte_reg.bits) + + // 5. Make the single, unconditional assignments to the registers + next_miso_byte_reg.valid := next_miso_byte_reg_valid_next + next_miso_byte_reg.bits := next_miso_byte_reg_bits_next + miso_buffer_reg.valid := miso_buffer_reg_valid_next + miso_buffer_reg.bits := miso_buffer_reg_bits_next } // === TileLink FSM Logic === @@ -243,7 +379,7 @@ )) tl_d_q.io.deq.ready := MuxCase(false.B, Seq( - read_fsm_active -> (tl_read_state_reg === TlReadState.sWaitBeatAck), + read_fsm_active -> (tl_read_state_reg === TlReadState.sWaitBeatAck && tl_to_spi_bulk_q.io.enq.ready), write_fsm_active -> (tl_write_state_reg === TlWriteState.sWaitBeatAck) )) @@ -259,20 +395,13 @@ a_bits.address := Mux(write_fsm_active, tl_write_addr_fsm_reg + (tl_write_beat_count_reg << log2Ceil(tlul_p.w)), tl_addr_fsm_reg + (tl_beat_count_reg << log2Ceil(tlul_p.w))) - a_bits.data := Mux(write_fsm_active, data_buffer(tl_write_beat_count_reg(3,0)), 0.U) + a_bits.data := Mux(write_fsm_active, write_data_buffer(tl_write_beat_count_reg(3,0)), 0.U) tl_a_q.io.enq.bits := a_bits - val reading_tl = tl_read_state_reg === TlReadState.sWaitBeatAck && - tl_d_q.io.deq.fire && - !tl_d_q.io.deq.bits.error - for (i <- 0 until data_buffer.length) { + for (i <- 0 until write_data_buffer.length) { val write_to_buffer = i.U === write_word_index && writing_data_buf - val read_from_buffer = i.U === tl_beat_count_reg(3,0) && reading_tl - data_buffer(i) := MuxCase(data_buffer(i), Seq( - write_to_buffer -> write_new_word, - read_from_buffer -> tl_d_q.io.deq.bits.data, - )) + write_data_buffer(i) := Mux(write_to_buffer, write_new_word, write_data_buffer(i)) } val clear_command = tl_cmd_reg_write && tl_cmd_reg_data === 0.U
diff --git a/kelvin_test_utils/BUILD b/kelvin_test_utils/BUILD index 89b0269..06b6f25 100644 --- a/kelvin_test_utils/BUILD +++ b/kelvin_test_utils/BUILD
@@ -29,11 +29,18 @@ srcs = ["spi_master.py"], deps = [ requirement("cocotb"), + ":spi_constants", ], visibility = ["//visibility:public"], ) py_library( + name = "spi_constants", + srcs = ["spi_constants.py"], + visibility = ["//visibility:public"], +) + +py_library( name = "secded_golden", srcs = ["secded_golden.py"], visibility = ["//visibility:public"],
diff --git a/kelvin_test_utils/spi_constants.py b/kelvin_test_utils/spi_constants.py new file mode 100644 index 0000000..197e63f --- /dev/null +++ b/kelvin_test_utils/spi_constants.py
@@ -0,0 +1,42 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import IntEnum + +class SpiRegAddress(IntEnum): + TL_ADDR_REG_0 = 0x00 + TL_ADDR_REG_1 = 0x01 + TL_ADDR_REG_2 = 0x02 + TL_ADDR_REG_3 = 0x03 + TL_LEN_REG = 0x04 + TL_CMD_REG = 0x05 + TL_STATUS_REG = 0x06 + DATA_BUF_PORT = 0x07 + TL_WRITE_STATUS_REG = 0x08 + BULK_WRITE_PORT = 0x09 + BULK_READ_PORT = 0x0A + BULK_READ_STATUS_REG = 0x0B + +class SpiCommand(IntEnum): + CMD_NULL = 0x00 + CMD_READ_START = 0x01 + CMD_WRITE_START = 0x02 + +class TlStatus(IntEnum): + IDLE = 0x00 + BUSY = 0x01 + DONE = 0x02 + ERROR = 0xFF + +CMD_WRITE = 0x80
diff --git a/kelvin_test_utils/spi_master.py b/kelvin_test_utils/spi_master.py index 32fe33e..060fba5 100644 --- a/kelvin_test_utils/spi_master.py +++ b/kelvin_test_utils/spi_master.py
@@ -15,6 +15,8 @@ import cocotb from cocotb.clock import Clock from cocotb.triggers import ClockCycles, FallingEdge +from kelvin_test_utils.spi_constants import SpiRegAddress, SpiCommand, TlStatus, CMD_WRITE + class SPIMaster: def __init__(self, clk, csb, mosi, miso, main_clk, log): @@ -65,7 +67,6 @@ await self.start_clock() byte_in = await self._clock_byte(byte_out) - await ClockCycles(self.clk, 2) await self.stop_clock() # Provide a hold time for CSb after the clock stops @@ -76,7 +77,7 @@ async def write_reg(self, reg_addr, data, wait_cycles=10): """Writes a byte to a register via SPI.""" - write_cmd = (1 << 7) | reg_addr + write_cmd = CMD_WRITE | reg_addr await self.spi_transaction(write_cmd) await self.spi_transaction(data) if wait_cycles > 0: @@ -92,6 +93,19 @@ read_data = await self.spi_transaction(0x00) return read_data + async def read_spi_domain_reg(self, reg_addr): + """Reads a byte from a register that lives in the SPI clock domain.""" + await self._set_cs(True) + await ClockCycles(self.main_clk, 1) + await self.start_clock() + await self._clock_byte(reg_addr) + read_data = await self._clock_byte(0x00) + await self.stop_clock() + await ClockCycles(self.main_clk, 1) + await self._set_cs(False) + await ClockCycles(self.main_clk, 1) + return read_data + async def poll_reg_for_value(self, reg_addr, expected_value, max_polls=20): """Polls a register until it reads an expected value.""" read_cmd = reg_addr # MSB is 0 for read @@ -112,69 +126,102 @@ self.log.error(f"Timed out after {max_polls} polls waiting for register 0x{reg_addr:x} to be 0x{expected_value:x}, got 0x{read_data:x}") return False - async def bulk_read_data(self, reg_addr, num_bytes): - """Reads a block of data from a pipelined port.""" - read_cmd = reg_addr + async def packed_write_transaction(self, target_addr, data): + """Writes a block of data using a packed SPI transaction. - # The read pipeline is two stages deep. We need to send two commands - # to discard two junk bytes before the first valid data byte is received. - for _ in range(2): - await self.spi_transaction(read_cmd) - await ClockCycles(self.main_clk, 10) - await self.idle_clocking(5) - await ClockCycles(self.main_clk, 10) - - # Read the valid bytes. - received_bytes = [] - for _ in range(num_bytes): - read_byte = await self.spi_transaction(read_cmd) - received_bytes.append(read_byte) - await ClockCycles(self.main_clk, 5) - - # Assemble the received bytes into a single large integer - read_data = 0 - for i, byte in enumerate(received_bytes): - read_data |= (byte << (i * 8)) - - return read_data - - async def bulk_write_data(self, reg_addr, data, num_bytes): - """Writes a block of data to a port.""" - for i in range(num_bytes): - byte = (data >> (i * 8)) & 0xFF - await self.write_reg(reg_addr, byte, wait_cycles=5) - - async def packed_write_transaction(self, target_addr, num_beats, data_generator): + Args: + target_addr: The starting address for the write. + data: A list of 128-bit integers to write. + """ await self._set_cs(True) await ClockCycles(self.main_clk, 1) await self.start_clock() # Write addr - await self._clock_byte(0x80) + await self._clock_byte(CMD_WRITE | SpiRegAddress.TL_ADDR_REG_0) await self._clock_byte((target_addr >> 0) & 0xFF) - await self._clock_byte(0x81) + await self._clock_byte(CMD_WRITE | SpiRegAddress.TL_ADDR_REG_1) await self._clock_byte((target_addr >> 8) & 0xFF) - await self._clock_byte(0x82) + await self._clock_byte(CMD_WRITE | SpiRegAddress.TL_ADDR_REG_2) await self._clock_byte((target_addr >> 16) & 0xFF) - await self._clock_byte(0x83) + await self._clock_byte(CMD_WRITE | SpiRegAddress.TL_ADDR_REG_3) await self._clock_byte((target_addr >> 24) & 0xFF) # Write beats - await self._clock_byte(0x84) - await self._clock_byte(num_beats - 1) + await self._clock_byte(CMD_WRITE | SpiRegAddress.TL_LEN_REG) + await self._clock_byte(len(data) - 1) - # Write data - for j in range(num_beats): - data = data_generator(j) + # Write data using bulk transfer + all_data_bytes = [] + for beat in data: for i in range(16): - byte = (data >> (i * 8)) & 0xFF - await self._clock_byte(0x87) - await self._clock_byte(byte) + all_data_bytes.append((beat >> (i * 8)) & 0xFF) - await self._clock_byte(0x85) - await self._clock_byte(0x02) + # Command for bulk write + await self._clock_byte(CMD_WRITE | SpiRegAddress.BULK_WRITE_PORT) + # Length + await self._clock_byte(len(all_data_bytes) - 1) + # Data stream + for byte in all_data_bytes: + await self._clock_byte(byte) + + await self._clock_byte(CMD_WRITE | SpiRegAddress.TL_CMD_REG) + await self._clock_byte(SpiCommand.CMD_WRITE_START) await self.stop_clock() await ClockCycles(self.main_clk, 1) await self._set_cs(False) + + async def bulk_write(self, data: list[int]): + """Writes a block of data using a single bulk SPI transaction.""" + await self._set_cs(True) + await ClockCycles(self.main_clk, 1) + + await self.start_clock() + + # Command byte for bulk write + await self._clock_byte(CMD_WRITE | SpiRegAddress.BULK_WRITE_PORT) + + # Length byte + num_bytes = len(data) + await self._clock_byte(num_bytes - 1) + + # Data stream + for byte in data: + await self._clock_byte(byte) + + await self.stop_clock() + await ClockCycles(self.main_clk, 1) + await self._set_cs(False) + + async def bulk_read(self, num_bytes: int) -> list[int]: + """Reads a block of data using a single bulk SPI transaction.""" + await self._set_cs(True) + await ClockCycles(self.main_clk, 1) + + await self.start_clock() + + # Command byte to initiate a bulk read (this is a WRITE command) + await self._clock_byte(CMD_WRITE | SpiRegAddress.BULK_READ_PORT) + + # Length byte + await self._clock_byte(num_bytes - 1) + + # The MISO pipeline is two bytes deep. We need to send two dummy transfers + # to discard the junk bytes from the command/length phases before the + # first valid data byte is received. + await self._clock_byte(0x00) # Flush junk from command phase + + # Read data stream + received_bytes = [] + for _ in range(num_bytes): + # The data is clocked out on MISO during this dummy byte transfer + byte_in = await self._clock_byte(0x00) + received_bytes.append(byte_in) + + await self.stop_clock() + await ClockCycles(self.main_clk, 1) + await self._set_cs(False) + + return received_bytes
diff --git a/tests/cocotb/tlul/BUILD b/tests/cocotb/tlul/BUILD index e659183..c8f4683 100644 --- a/tests/cocotb/tlul/BUILD +++ b/tests/cocotb/tlul/BUILD
@@ -355,6 +355,9 @@ "test_tlul_write", "test_tlul_multi_beat_write", "test_packed_write_transaction", + "test_tlul_bulk_write", + "test_tlul_bulk_read", + "test_large_tlul_transfer", ] # END_TESTCASES_FOR_spi2tlul_cocotb
diff --git a/tests/cocotb/tlul/test_spi_to_tlul.py b/tests/cocotb/tlul/test_spi_to_tlul.py index 0b4059b..419d68e 100644 --- a/tests/cocotb/tlul/test_spi_to_tlul.py +++ b/tests/cocotb/tlul/test_spi_to_tlul.py
@@ -14,17 +14,22 @@ import cocotb import random +import os +import math from cocotb.clock import Clock from cocotb.triggers import RisingEdge, ClockCycles, FallingEdge from kelvin_test_utils.TileLinkULInterface import TileLinkULInterface from kelvin_test_utils.spi_master import SPIMaster -async def setup_dut(dut): +async def setup_dut(dut, spi_master): # Main clock started by the test dut.io_spi_csb.value = 1 # Start with chip select inactive dut.reset.value = 1 - await ClockCycles(dut.clock, 2) + await spi_master.start_clock() + await ClockCycles(dut.clock, 5) # Ensure reset assertion is sampled dut.reset.value = 0 + await ClockCycles(dut.clock, 5) # Ensure reset de-assertion is sampled + await spi_master.stop_clock() await RisingEdge(dut.clock) @cocotb.test() @@ -33,7 +38,6 @@ clock = Clock(dut.clock, 10) cocotb.start_soon(clock.start()) - await setup_dut(dut) spi_master = SPIMaster( clk=dut.io_spi_clk, csb=dut.io_spi_csb, @@ -42,6 +46,7 @@ main_clk=dut.clock, log=dut._log ) + await setup_dut(dut, spi_master) # Write Transaction write_data = random.randint(0, 255) @@ -60,7 +65,6 @@ clock = Clock(dut.clock, 10) cocotb.start_soon(clock.start()) - await setup_dut(dut) spi_master = SPIMaster( clk=dut.io_spi_clk, csb=dut.io_spi_csb, @@ -69,6 +73,7 @@ main_clk=dut.clock, log=dut._log ) + await setup_dut(dut, spi_master) tl_device = TileLinkULInterface(dut, device_if_name="io_tl", width=128) await tl_device.init() @@ -112,10 +117,17 @@ # 1. Poll the status register until the transaction is done assert await spi_master.poll_reg_for_value(0x06, 0x02), "Timed out waiting for status to be Done" - # 2. Read the data from the buffer port - read_data = await spi_master.bulk_read_data(0x07, 16) + # 2. Check that the correct number of bytes are available + bytes_available = await spi_master.read_spi_domain_reg(0x0B) + assert bytes_available == 16 - # 3. Compare with expected data + # 3. Read the data from the buffer port using the new bulk read + read_data_bytes = await spi_master.bulk_read(16) + read_data = 0 + for j, byte in enumerate(read_data_bytes): + read_data |= (byte << (j * 8)) + + # 4. Compare with expected data expected_data = 0xDEADBEEF_CAFEF00D_ABAD1DEA_C0DED00D + i assert read_data == expected_data @@ -131,7 +143,6 @@ clock = Clock(dut.clock, 10) cocotb.start_soon(clock.start()) - await setup_dut(dut) spi_master = SPIMaster( clk=dut.io_spi_clk, csb=dut.io_spi_csb, @@ -140,6 +151,7 @@ main_clk=dut.clock, log=dut._log ) + await setup_dut(dut, spi_master) tl_device = TileLinkULInterface(dut, device_if_name="io_tl", width=128) await tl_device.init() @@ -187,11 +199,18 @@ # 1. Poll the status register until the transaction is done assert await spi_master.poll_reg_for_value(0x06, 0x02), "Timed out waiting for status to be Done" - # 2. Read the data from the buffer port + # 2. Check that the correct number of bytes are available bytes_to_read = num_beats * 16 - read_data = await spi_master.bulk_read_data(0x07, bytes_to_read) + bytes_available = await spi_master.read_spi_domain_reg(0x0B) + assert bytes_available == bytes_to_read - # 3. Compare with expected data + # 3. Read the data from the buffer port using the new bulk read + read_data_bytes = await spi_master.bulk_read(bytes_to_read) + read_data = 0 + for i, byte in enumerate(read_data_bytes): + read_data |= (byte << (i * 8)) + + # 4. Compare with expected data expected_data = 0 for i in range(num_beats): word = 0xDEADBEEF_CAFEF00D_ABAD1DEA_C0DED00D + i @@ -211,7 +230,6 @@ clock = Clock(dut.clock, 10) cocotb.start_soon(clock.start()) - await setup_dut(dut) spi_master = SPIMaster( clk=dut.io_spi_clk, csb=dut.io_spi_csb, @@ -220,6 +238,7 @@ main_clk=dut.clock, log=dut._log ) + await setup_dut(dut, spi_master) tl_device = TileLinkULInterface(dut, device_if_name="io_tl", width=128) await tl_device.init() @@ -254,7 +273,8 @@ # 1. Write data to the DUT's internal buffer write_data = 0x11223344_55667788_99AABBCC_DDEEFF00 + i expected_data_list.append(write_data) - await spi_master.bulk_write_data(0x07, write_data, 16) + write_data_bytes = list(write_data.to_bytes(16, 'little')) + await spi_master.bulk_write(write_data_bytes) # 2. Configure the TileLink write via SPI target_addr = 0x40002000 + (i * 16) @@ -290,7 +310,6 @@ clock = Clock(dut.clock, 10) cocotb.start_soon(clock.start()) - await setup_dut(dut) spi_master = SPIMaster( clk=dut.io_spi_clk, csb=dut.io_spi_csb, @@ -299,6 +318,7 @@ main_clk=dut.clock, log=dut._log ) + await setup_dut(dut, spi_master) tl_device = TileLinkULInterface(dut, device_if_name="io_tl", width=128) await tl_device.init() @@ -328,14 +348,13 @@ # --- Main Test Logic --- # 1. Prepare and write data to the DUT's internal buffer expected_data_list = [] - full_write_data = 0 + write_data_bytes = [] for i in range(num_beats): word = 0x11223344_55667788_99AABBCC_DDEEFF00 + i expected_data_list.append(word) - full_write_data |= (word << (i * 128)) + write_data_bytes.extend(list(word.to_bytes(16, 'little'))) - bytes_to_write = num_beats * 16 - await spi_master.bulk_write_data(0x07, full_write_data, bytes_to_write) + await spi_master.bulk_write(write_data_bytes) # 2. Configure the TileLink write via SPI target_addr = 0x40002000 @@ -369,7 +388,6 @@ clock = Clock(dut.clock, 10) cocotb.start_soon(clock.start()) - await setup_dut(dut) spi_master = SPIMaster( clk=dut.io_spi_clk, csb=dut.io_spi_csb, @@ -378,6 +396,7 @@ main_clk=dut.clock, log=dut._log ) + await setup_dut(dut, spi_master) tl_device = TileLinkULInterface(dut, device_if_name="io_tl", width=128) await tl_device.init() @@ -400,16 +419,312 @@ responder_task = cocotb.start_soon(device_responder()) - def data_generator(beat): - return 0xDEADBEEF_CAFEF00D_ABAD1DEA_C0DED00D + beat - + data = [0xDEADBEEF_CAFEF00D_ABAD1DEA_C0DED00D + i for i in range(num_beats)] await spi_master.packed_write_transaction( target_addr=0x40001000, - num_beats=num_beats, - data_generator=data_generator + data=data, ) assert await spi_master.poll_reg_for_value(0x08, 0x02), "Timed out waiting for write status to be Done" await spi_master.write_reg(0x05, 0x00) await responder_task + +@cocotb.test() +async def test_tlul_bulk_write(dut): + """Tests a TileLink UL write transaction initiated via the new bulk SPI write.""" + # Start the main clock + clock = Clock(dut.clock, 10) + cocotb.start_soon(clock.start()) + + spi_master = SPIMaster( + clk=dut.io_spi_clk, + csb=dut.io_spi_csb, + mosi=dut.io_spi_mosi, + miso=dut.io_spi_miso, + main_clk=dut.clock, + log=dut._log + ) + await setup_dut(dut, spi_master) + tl_device = TileLinkULInterface(dut, device_if_name="io_tl", width=128) + await tl_device.init() + + num_beats = 2 + expected_data_list = [] + write_data_bytes = [] + + for i in range(num_beats): + word = 0xCAFEF00D_DEADBEEF_C0DED00D_ABAD1DEA + i + expected_data_list.append(word) + for j in range(16): + write_data_bytes.append((word >> (j * 8)) & 0xFF) + + # --- Device Responder Task --- + received_data_list = [] + async def device_responder(): + for _ in range(num_beats): + req = await tl_device.device_get_request() + assert int(req['opcode']) in [0, 1] + received_data_list.append(int(req['data'])) + await tl_device.device_respond( + opcode=0, + param=0, + size=req['size'], + source=req['source'], + error=0, + width=128 + ) + + responder_task = cocotb.start_soon(device_responder()) + + # --- Main Test Logic --- + # 1. Write data to the DUT's internal buffer using the new bulk write method + await spi_master.bulk_write(write_data_bytes) + + # 2. Configure the TileLink write via SPI + target_addr = 0x40003000 + for j in range(4): + addr_byte = (target_addr >> (j * 8)) & 0xFF + await spi_master.write_reg(0x00 + j, addr_byte) + + await spi_master.write_reg(0x04, num_beats - 1) + + # 3. Issue the write command + await spi_master.write_reg(0x05, 0x02, wait_cycles=20) + + # --- Verification --- + # 1. Poll the status register until the transaction is done + assert await spi_master.poll_reg_for_value(0x08, 0x02), "Timed out waiting for write status to be Done" + + # 2. Wait for the responder to finish + await responder_task + + # 3. Verify the data received by the responder + assert len(received_data_list) == num_beats + assert received_data_list == expected_data_list + + # 4. Clear the status to return FSM to Idle + await spi_master.write_reg(0x05, 0x00) + + +@cocotb.test() +async def test_tlul_bulk_read(dut): + """Tests a TileLink UL read transaction initiated via SPI and read via the new bulk SPI read.""" + # Start the main clock + clock = Clock(dut.clock, 10) + cocotb.start_soon(clock.start()) + + spi_master = SPIMaster( + clk=dut.io_spi_clk, + csb=dut.io_spi_csb, + mosi=dut.io_spi_mosi, + miso=dut.io_spi_miso, + main_clk=dut.clock, + log=dut._log + ) + await setup_dut(dut, spi_master) + tl_device = TileLinkULInterface(dut, device_if_name="io_tl", width=128) + await tl_device.init() + + num_beats = 2 + expected_data_list = [] + for i in range(num_beats): + word = 0xABAD1DEA_C0DED00D_DEADBEEF_CAFEF00D + i + expected_data_list.append(word) + + # --- Device Responder Task --- + async def device_responder(): + for i in range(num_beats): + req = await tl_device.device_get_request() + assert int(req['opcode']) == 4, f"Expected Get opcode (4), got {req['opcode']}" + await tl_device.device_respond( + opcode=1, # AccessAckData + param=0, + size=req['size'], + source=req['source'], + data=expected_data_list[i], + error=0, + width=128 + ) + + responder_task = cocotb.start_soon(device_responder()) + + # --- Main Test Logic --- + # 1. Configure the TileLink read via SPI + target_addr = 0x40001000 + for j in range(4): + addr_byte = (target_addr >> (j * 8)) & 0xFF + await spi_master.write_reg(0x00 + j, addr_byte) + + await spi_master.write_reg(0x04, num_beats - 1) + + # 2. Issue the read command + await spi_master.write_reg(0x05, 0x01, wait_cycles=0) + + # 3. Poll the status register until the transaction is done + assert await spi_master.poll_reg_for_value(0x06, 0x02), "Timed out waiting for status to be Done" + + # 3a. Read the bulk read status register + bytes_available = await spi_master.read_spi_domain_reg(0x0B) + dut._log.info(f"BULK_READ_STATUS_REG = {bytes_available}") + + # 4. Initiate the bulk read from the data buffer + num_bytes_to_read = num_beats * 16 + read_data_bytes = await spi_master.bulk_read(num_bytes_to_read) + + # --- Verification --- + # 1. Convert the received bytes back into words + read_data_list = [] + for i in range(num_beats): + word = 0 + for j in range(16): + word |= (read_data_bytes[i*16 + j] << (j * 8)) + read_data_list.append(word) + + # 2. Verify the data + assert read_data_list == expected_data_list, f"{[hex(x) for x in read_data_list]} =/= {[hex(x) for x in expected_data_list]}" + + # 3. Clear the status to return FSM to Idle + await spi_master.write_reg(0x05, 0x00) + + await responder_task + +@cocotb.test(timeout_time=300, timeout_unit="sec") +async def test_large_tlul_transfer(dut): + """Verify increasingly large transfers (up to 16KB) via Spi2TLUL.""" + # Start the main clock + clock = Clock(dut.clock, 10) + cocotb.start_soon(clock.start()) + + spi_master = SPIMaster( + clk=dut.io_spi_clk, + csb=dut.io_spi_csb, + mosi=dut.io_spi_mosi, + miso=dut.io_spi_miso, + main_clk=dut.clock, + log=dut._log + ) + await setup_dut(dut, spi_master) + tl_device = TileLinkULInterface(dut, device_if_name="io_tl", width=128) + await tl_device.init() + + # Test parameters + base_addr = 0x20000000 + write_chunk_size = 256 # Max for bulk_write + read_chunk_size = 128 # As requested + bus_width_bytes = 128 // 8 + + transfer_sizes = [1024 * x for x in [1, 2, 4, 8, 16]] + + for total_size in transfer_sizes: + dut._log.info(f"--- Starting {total_size // 1024}KB Transfer Test ---") + + # Generate random data + dut._log.info(f"Generating {total_size // 1024}KB of random data...") + golden_data = os.urandom(total_size) + dut._log.info("Data generation complete.") + + # --- Device Responder Task --- + async def device_responder(): + mem = {} + total_bytes_written = 0 + total_beats_to_write = total_size // bus_width_bytes + + # --- Write Phase --- + dut._log.info(f"Device responder waiting for {total_size} bytes...") + for _ in range(total_beats_to_write): + req = await tl_device.device_get_request() + assert int(req['opcode']) in [0, 1] + + addr = int(req['address']) + data = int(req['data']) + mask = int(req['mask']) + + for byte_idx in range(bus_width_bytes): + if (mask >> byte_idx) & 1: + byte_val = (data >> (byte_idx * 8)) & 0xFF + mem[addr + byte_idx] = byte_val + + await tl_device.device_respond( + opcode=0, param=0, size=req['size'], source=req['source'], error=0, width=128 + ) + total_bytes_written += bin(mask).count('1') + dut._log.info(f"Device responder received {total_bytes_written} bytes.") + + # --- Read Phase --- + dut._log.info(f"Device responder waiting for read requests...") + total_bytes_read = 0 + while total_bytes_read < total_size: + req = await tl_device.device_get_request() + assert int(req['opcode']) == 4 + + addr = int(req['address']) + size = int(req['size']) + num_bytes = 1 << size + + response_data = 0 + for i in range(num_bytes): + byte_val = mem.get(addr + i, 0) + response_data |= (byte_val << (i * 8)) + + await tl_device.device_respond( + opcode=1, param=0, size=size, source=req['source'], data=response_data, error=0, width=128 + ) + total_bytes_read += num_bytes + dut._log.info(f"Device responder sent {total_bytes_read} bytes.") + + responder_task = cocotb.start_soon(device_responder()) + + # --- Main Test Logic: Write Phase --- + dut._log.info(f"Starting {total_size // 1024}KB write phase...") + for i in range(0, total_size, write_chunk_size): + chunk = golden_data[i:i + write_chunk_size] + num_beats = int(math.ceil(len(chunk) / bus_width_bytes)) + current_addr = base_addr + i + + await spi_master.bulk_write(list(chunk)) + + for j in range(4): + addr_byte = (current_addr >> (j * 8)) & 0xFF + await spi_master.write_reg(0x00 + j, addr_byte) + + await spi_master.write_reg(0x04, num_beats - 1) + await spi_master.write_reg(0x05, 0x02, wait_cycles=20) + + assert await spi_master.poll_reg_for_value(0x08, 0x02), f"Timed out waiting for write status at addr {current_addr:x}" + await spi_master.write_reg(0x05, 0x00) + dut._log.info("Write phase complete.") + + # --- Main Test Logic: Read Phase --- + dut._log.info(f"Starting {total_size // 1024}KB read phase...") + read_back_data = bytearray() + for i in range(0, total_size, read_chunk_size): + num_beats = int(math.ceil(read_chunk_size / bus_width_bytes)) + current_addr = base_addr + i + + for j in range(4): + addr_byte = (current_addr >> (j * 8)) & 0xFF + await spi_master.write_reg(0x00 + j, addr_byte) + + await spi_master.write_reg(0x04, num_beats - 1) + await spi_master.write_reg(0x05, 0x01, wait_cycles=0) + + assert await spi_master.poll_reg_for_value(0x06, 0x02), f"Timed out waiting for read status at addr {current_addr:x}" + + bytes_available = await spi_master.read_spi_domain_reg(0x0B) + assert bytes_available == read_chunk_size + + read_data_bytes = await spi_master.bulk_read(read_chunk_size) + read_back_data.extend(read_data_bytes) + + await spi_master.write_reg(0x05, 0x00) + dut._log.info("Read phase complete.") + + # --- Verification --- + dut._log.info(f"Verifying {total_size // 1024}KB of data...") + assert read_back_data == golden_data, "Read-back data does not match golden data" + dut._log.info(f"Data verification successful for {total_size // 1024}KB!") + + await responder_task + dut._log.info(f"--- {total_size // 1024}KB Transfer Test Passed ---") +