diff --git a/axi/tb/axi_cocotb_loopback_test/axi4_proxy.py b/axi/tb/axi_cocotb_loopback_test/axi4_proxy.py new file mode 100644 index 0000000..36d2158 --- /dev/null +++ b/axi/tb/axi_cocotb_loopback_test/axi4_proxy.py @@ -0,0 +1,430 @@ +# proxy to match cocotb and custom axi4_if + +import logging + +from cocotb.types import LogicArray + +from cocotbext.axi.axi_channels import ( + AxiAWBus, + AxiWBus, + AxiBBus, + AxiARBus, + AxiRBus, + AxiBus, +) + + +def install_cocotbext_axi_slice_compat(): + """ + cocotbext-axi StreamMonitor uses RisingEdge(self.valid/self.ready). + That works only for real cocotb LogicObject handles. + + Our SliceSignal is a Python proxy over packed req/resp vector, so + RisingEdge(SliceSignal) fails in cocotb 2.x. + + This patch makes StreamMonitor wake on signal.value_change instead. + For SliceSignal this should return parent packed vector value_change. + """ + + # this sucks... + from cocotbext.axi import stream as axi_stream + + async def _run_valid_monitor_value_change(self): + while True: + await self.valid.value_change + self.wake_event.set() + + async def _run_ready_monitor_value_change(self): + while True: + await self.ready.value_change + self.wake_event.set() + + axi_stream.StreamMonitor._run_valid_monitor = _run_valid_monitor_value_change + axi_stream.StreamMonitor._run_ready_monitor = _run_ready_monitor_value_change + + +def _safe_int(value) -> int: + try: + return int(value) + except Exception: + + s = str(value).upper() + bits = [] + + for ch in s: + if ch in "01": + bits.append(ch) + elif ch in "XZUW-": + bits.append("0") + + if not bits: + return 0 + + return int("".join(bits), 2) + + +def _logic_array(value: int, width: int): + value &= (1 << width) - 1 + return LogicArray.from_unsigned(value, width) + + +def _param_int(obj, name: str, override): + if override is not None: + return int(override) + + p = getattr(obj, name) + + try: + return int(p) + except Exception: + pass + + try: + return int(p.value) + except Exception: + pass + + raise RuntimeError(f"Cannot read parameter {name} from {obj!r}") + + +def _packed_offsets(fields): + """ + de-construct SV signal order. + """ + total = sum(width for _, width in fields) + pos = total + offsets = {} + + for name, width in fields: + pos -= width + offsets[name] = (pos, width) + + return total, offsets + + +class PackedVector: + def __init__(self, handle, name=None): + self.handle = handle + self.name = name or getattr(handle, "_path", repr(handle)) + self.width = len(handle) + self.mask = (1 << self.width) - 1 + + self.shadow = _safe_int(handle.value) & self.mask + self.dirty = False + + def read(self, lo: int, width: int) -> int: + src = self.shadow if self.dirty else ( + _safe_int(self.handle.value) & self.mask) + return (src >> lo) & ((1 << width) - 1) + + def write(self, lo: int, width: int, value, immediate=False): + value = _safe_int(value) + field_mask = ((1 << width) - 1) << lo + + self.shadow &= ~field_mask + self.shadow |= (value << lo) & field_mask + self.shadow &= self.mask + self.dirty = True + + v = _logic_array(self.shadow, self.width) + + if immediate: + self.handle.setimmediatevalue(v) + else: + self.handle.value = v + + +class SliceSignal: + """ + cocotbext-axi proxy + + looks like: + sig.value + sig.value = ... + sig.setimmediatevalue(...) + len(sig) + + """ + + def __init__(self, name: str, parent: PackedVector, lo: int, width: int): + self._name = name + self._path = name + self._log = logging.getLogger(f"cocotb.{name}") + + self.parent = parent + self.lo = lo + self.width = width + + def __len__(self): + return self.width + + def __repr__(self): + return f"" + + @property + def value(self): + return _logic_array(self.parent.read(self.lo, self.width), self.width) + + @value.setter + def value(self, value): + self.parent.write(self.lo, self.width, value, immediate=False) + + def setimmediatevalue(self, value): + self.parent.write(self.lo, self.width, value, immediate=True) + + def set(self, value): + self.value = value + + def get(self): + return self.value + + @property + def value_change(self): + # dirty + return self.parent.handle.value_change + + +class ProxyEntity: + def __init__(self, name: str, **signals): + self._name = name + self._path = name + self._log = logging.getLogger(f"cocotb.{name}") + self.__dict__.update(signals) + + def __dir__(self): + return list(self.__dict__.keys()) + + +def _sig(name, parent, base_lo, layout, field): + lo, width = layout[field] + return SliceSignal(name, parent, base_lo + lo, width) + + +def axi4_bus_from_packed_if( + iface, + name="axi", + *, + addr_width=None, + data_width=None, + id_width=None, + user_width=None, +): + """ + Build cocotbext-axi AxiBus from custom packed axi4_if. + + iface should be instance axi4_if: + dut.axi_in + dut.axi_out + + example: + master = AxiMaster( + axi4_bus_from_packed_if(dut.axi_in, "s_axi"), + dut.clk, + dut.rst, + ) + + ram = AxiRam( + axi4_bus_from_packed_if(dut.axi_out, "m_axi"), + dut.clk, + dut.rst, + size=2**20, + ) + """ + + ADDR_W = _param_int(iface, "ADDR_W", addr_width) + DATA_W = _param_int(iface, "DATA_W", data_width) + ID_W = _param_int(iface, "ID_W", id_width) + USER_W = _param_int(iface, "USER_W", user_width) + + if DATA_W % 8 != 0: + raise ValueError(f"AXI DATA_W must be divisible by 8, got {DATA_W}") + + STRB_W = DATA_W // 8 + + aw_total, aw_layout = _packed_offsets([ + ("id", ID_W), + ("addr", ADDR_W), + ("len", 8), + ("size", 3), + ("burst", 2), + ("lock", 1), + ("cache", 4), + ("prot", 3), + ("qos", 4), + ("region", 4), + ("user", USER_W), + ("valid", 1), + ]) + + w_total, w_layout = _packed_offsets([ + ("data", DATA_W), + ("strb", STRB_W), + ("last", 1), + ("user", USER_W), + ("valid", 1), + ]) + + b_total, b_layout = _packed_offsets([ + ("id", ID_W), + ("resp", 2), + ("user", USER_W), + ("valid", 1), + ]) + + ar_total, ar_layout = _packed_offsets([ + ("id", ID_W), + ("addr", ADDR_W), + ("len", 8), + ("size", 3), + ("burst", 2), + ("lock", 1), + ("cache", 4), + ("prot", 3), + ("qos", 4), + ("region", 4), + ("user", USER_W), + ("valid", 1), + ]) + + r_total, r_layout = _packed_offsets([ + ("id", ID_W), + ("data", DATA_W), + ("resp", 2), + ("last", 1), + ("user", USER_W), + ("valid", 1), + ]) + + req_total, req_layout = _packed_offsets([ + ("aw", aw_total), + ("w", w_total), + ("b_ready", 1), + ("ar", ar_total), + ("r_ready", 1), + ]) + + resp_total, resp_layout = _packed_offsets([ + ("aw_ready", 1), + ("w_ready", 1), + ("b", b_total), + ("ar_ready", 1), + ("r", r_total), + ]) + + req = PackedVector(iface.req, f"{name}.req") + resp = PackedVector(iface.resp, f"{name}.resp") + + if req_total != req.width: + raise RuntimeError( + f"{name}.req layout mismatch: calculated {req_total} bits, " + f"simulator has {req.width} bits" + ) + + if resp_total != resp.width: + raise RuntimeError( + f"{name}.resp layout mismatch: calculated {resp_total} bits, " + f"simulator has {resp.width} bits" + ) + + aw_lo, _ = req_layout["aw"] + w_lo, _ = req_layout["w"] + ar_lo, _ = req_layout["ar"] + + b_lo, _ = resp_layout["b"] + r_lo, _ = resp_layout["r"] + + aw = AxiAWBus.from_entity(ProxyEntity( + f"{name}_aw", + + awid=_sig(f"{name}_awid", req, aw_lo, aw_layout, "id"), + awaddr=_sig(f"{name}_awaddr", req, aw_lo, aw_layout, "addr"), + awlen=_sig(f"{name}_awlen", req, aw_lo, aw_layout, "len"), + awsize=_sig(f"{name}_awsize", req, aw_lo, aw_layout, "size"), + awburst=_sig(f"{name}_awburst", req, aw_lo, aw_layout, "burst"), + awlock=_sig(f"{name}_awlock", req, aw_lo, aw_layout, "lock"), + awcache=_sig(f"{name}_awcache", req, aw_lo, aw_layout, "cache"), + awprot=_sig(f"{name}_awprot", req, aw_lo, aw_layout, "prot"), + awqos=_sig(f"{name}_awqos", req, aw_lo, aw_layout, "qos"), + awregion=_sig(f"{name}_awregion", req, aw_lo, aw_layout, "region"), + awuser=_sig(f"{name}_awuser", req, aw_lo, aw_layout, "user"), + awvalid=_sig(f"{name}_awvalid", req, aw_lo, aw_layout, "valid"), + + awready=SliceSignal( + f"{name}_awready", + resp, + *resp_layout["aw_ready"], + ), + )) + + w = AxiWBus.from_entity(ProxyEntity( + f"{name}_w", + + wdata=_sig(f"{name}_wdata", req, w_lo, w_layout, "data"), + wstrb=_sig(f"{name}_wstrb", req, w_lo, w_layout, "strb"), + wlast=_sig(f"{name}_wlast", req, w_lo, w_layout, "last"), + wuser=_sig(f"{name}_wuser", req, w_lo, w_layout, "user"), + wvalid=_sig(f"{name}_wvalid", req, w_lo, w_layout, "valid"), + + wready=SliceSignal( + f"{name}_wready", + resp, + *resp_layout["w_ready"], + ), + )) + + b = AxiBBus.from_entity(ProxyEntity( + f"{name}_b", + + bid=_sig(f"{name}_bid", resp, b_lo, b_layout, "id"), + bresp=_sig(f"{name}_bresp", resp, b_lo, b_layout, "resp"), + buser=_sig(f"{name}_buser", resp, b_lo, b_layout, "user"), + bvalid=_sig(f"{name}_bvalid", resp, b_lo, b_layout, "valid"), + + bready=SliceSignal( + f"{name}_bready", + req, + *req_layout["b_ready"], + ), + )) + + ar = AxiARBus.from_entity(ProxyEntity( + f"{name}_ar", + + arid=_sig(f"{name}_arid", req, ar_lo, ar_layout, "id"), + araddr=_sig(f"{name}_araddr", req, ar_lo, ar_layout, "addr"), + arlen=_sig(f"{name}_arlen", req, ar_lo, ar_layout, "len"), + arsize=_sig(f"{name}_arsize", req, ar_lo, ar_layout, "size"), + arburst=_sig(f"{name}_arburst", req, ar_lo, ar_layout, "burst"), + arlock=_sig(f"{name}_arlock", req, ar_lo, ar_layout, "lock"), + arcache=_sig(f"{name}_arcache", req, ar_lo, ar_layout, "cache"), + arprot=_sig(f"{name}_arprot", req, ar_lo, ar_layout, "prot"), + arqos=_sig(f"{name}_arqos", req, ar_lo, ar_layout, "qos"), + arregion=_sig(f"{name}_arregion", req, ar_lo, ar_layout, "region"), + aruser=_sig(f"{name}_aruser", req, ar_lo, ar_layout, "user"), + arvalid=_sig(f"{name}_arvalid", req, ar_lo, ar_layout, "valid"), + + arready=SliceSignal( + f"{name}_arready", + resp, + *resp_layout["ar_ready"], + ), + )) + + r = AxiRBus.from_entity(ProxyEntity( + f"{name}_r", + + rid=_sig(f"{name}_rid", resp, r_lo, r_layout, "id"), + rdata=_sig(f"{name}_rdata", resp, r_lo, r_layout, "data"), + rresp=_sig(f"{name}_rresp", resp, r_lo, r_layout, "resp"), + rlast=_sig(f"{name}_rlast", resp, r_lo, r_layout, "last"), + ruser=_sig(f"{name}_ruser", resp, r_lo, r_layout, "user"), + rvalid=_sig(f"{name}_rvalid", resp, r_lo, r_layout, "valid"), + + rready=SliceSignal( + f"{name}_rready", + req, + *req_layout["r_ready"], + ), + )) + + return AxiBus.from_channels(aw, w, b, ar, r) diff --git a/axi/tb/axi_cocotb_loopback_test/tb_axi4_loopback.sv b/axi/tb/axi_cocotb_loopback_test/tb_axi4_loopback.sv index 9bcd721..5063d0d 100644 --- a/axi/tb/axi_cocotb_loopback_test/tb_axi4_loopback.sv +++ b/axi/tb/axi_cocotb_loopback_test/tb_axi4_loopback.sv @@ -1,113 +1,11 @@ -`timescale 1ns/1ps - module tb_axi4_loopback #( parameter int unsigned ADDR_W = 32, parameter int unsigned DATA_W = 64, parameter int unsigned ID_W = 4, parameter int unsigned USER_W = 1 )( - input logic clk, - input logic rst, - - // slave-side flat AXI port (cocotb driven) - input logic [ID_W-1:0] s_axi_awid, - input logic [ADDR_W-1:0] s_axi_awaddr, - input logic [7:0] s_axi_awlen, - input logic [2:0] s_axi_awsize, - input logic [1:0] s_axi_awburst, - input logic s_axi_awlock, - input logic [3:0] s_axi_awcache, - input logic [2:0] s_axi_awprot, - input logic [3:0] s_axi_awqos, - input logic [3:0] s_axi_awregion, - input logic [USER_W-1:0] s_axi_awuser, - input logic s_axi_awvalid, - output logic s_axi_awready, - - input logic [DATA_W-1:0] s_axi_wdata, - input logic [DATA_W/8-1:0] s_axi_wstrb, - input logic s_axi_wlast, - input logic [USER_W-1:0] s_axi_wuser, - input logic s_axi_wvalid, - output logic s_axi_wready, - - output logic [ID_W-1:0] s_axi_bid, - output logic [1:0] s_axi_bresp, - output logic [USER_W-1:0] s_axi_buser, - output logic s_axi_bvalid, - input logic s_axi_bready, - - input logic [ID_W-1:0] s_axi_arid, - input logic [ADDR_W-1:0] s_axi_araddr, - input logic [7:0] s_axi_arlen, - input logic [2:0] s_axi_arsize, - input logic [1:0] s_axi_arburst, - input logic s_axi_arlock, - input logic [3:0] s_axi_arcache, - input logic [2:0] s_axi_arprot, - input logic [3:0] s_axi_arqos, - input logic [3:0] s_axi_arregion, - input logic [USER_W-1:0] s_axi_aruser, - input logic s_axi_arvalid, - output logic s_axi_arready, - - output logic [ID_W-1:0] s_axi_rid, - output logic [DATA_W-1:0] s_axi_rdata, - output logic [1:0] s_axi_rresp, - output logic s_axi_rlast, - output logic [USER_W-1:0] s_axi_ruser, - output logic s_axi_rvalid, - input logic s_axi_rready, - - // master-side flat AXI port for coco-tb - output logic [ID_W-1:0] m_axi_awid, - output logic [ADDR_W-1:0] m_axi_awaddr, - output logic [7:0] m_axi_awlen, - output logic [2:0] m_axi_awsize, - output logic [1:0] m_axi_awburst, - output logic m_axi_awlock, - output logic [3:0] m_axi_awcache, - output logic [2:0] m_axi_awprot, - output logic [3:0] m_axi_awqos, - output logic [3:0] m_axi_awregion, - output logic [USER_W-1:0] m_axi_awuser, - output logic m_axi_awvalid, - input logic m_axi_awready, - - output logic [DATA_W-1:0] m_axi_wdata, - output logic [DATA_W/8-1:0] m_axi_wstrb, - output logic m_axi_wlast, - output logic [USER_W-1:0] m_axi_wuser, - output logic m_axi_wvalid, - input logic m_axi_wready, - - input logic [ID_W-1:0] m_axi_bid, - input logic [1:0] m_axi_bresp, - input logic [USER_W-1:0] m_axi_buser, - input logic m_axi_bvalid, - output logic m_axi_bready, - - output logic [ID_W-1:0] m_axi_arid, - output logic [ADDR_W-1:0] m_axi_araddr, - output logic [7:0] m_axi_arlen, - output logic [2:0] m_axi_arsize, - output logic [1:0] m_axi_arburst, - output logic m_axi_arlock, - output logic [3:0] m_axi_arcache, - output logic [2:0] m_axi_arprot, - output logic [3:0] m_axi_arqos, - output logic [3:0] m_axi_arregion, - output logic [USER_W-1:0] m_axi_aruser, - output logic m_axi_arvalid, - input logic m_axi_arready, - - input logic [ID_W-1:0] m_axi_rid, - input logic [DATA_W-1:0] m_axi_rdata, - input logic [1:0] m_axi_rresp, - input logic m_axi_rlast, - input logic [USER_W-1:0] m_axi_ruser, - input logic m_axi_rvalid, - output logic m_axi_rready + input logic clk, + input logic rst ); logic aresetn; @@ -133,120 +31,9 @@ module tb_axi4_loopback #( .aresetn(aresetn) ); - axi4_flat_to_if #( - .ADDR_W(ADDR_W), - .DATA_W(DATA_W), - .ID_W(ID_W), - .USER_W(USER_W) - ) u_flat_to_if ( - .s_axi_awid (s_axi_awid), - .s_axi_awaddr (s_axi_awaddr), - .s_axi_awlen (s_axi_awlen), - .s_axi_awsize (s_axi_awsize), - .s_axi_awburst (s_axi_awburst), - .s_axi_awlock (s_axi_awlock), - .s_axi_awcache (s_axi_awcache), - .s_axi_awprot (s_axi_awprot), - .s_axi_awqos (s_axi_awqos), - .s_axi_awregion (s_axi_awregion), - .s_axi_awuser (s_axi_awuser), - .s_axi_awvalid (s_axi_awvalid), - .s_axi_awready (s_axi_awready), - .s_axi_wdata (s_axi_wdata), - .s_axi_wstrb (s_axi_wstrb), - .s_axi_wlast (s_axi_wlast), - .s_axi_wuser (s_axi_wuser), - .s_axi_wvalid (s_axi_wvalid), - .s_axi_wready (s_axi_wready), - .s_axi_bid (s_axi_bid), - .s_axi_bresp (s_axi_bresp), - .s_axi_buser (s_axi_buser), - .s_axi_bvalid (s_axi_bvalid), - .s_axi_bready (s_axi_bready), - .s_axi_arid (s_axi_arid), - .s_axi_araddr (s_axi_araddr), - .s_axi_arlen (s_axi_arlen), - .s_axi_arsize (s_axi_arsize), - .s_axi_arburst (s_axi_arburst), - .s_axi_arlock (s_axi_arlock), - .s_axi_arcache (s_axi_arcache), - .s_axi_arprot (s_axi_arprot), - .s_axi_arqos (s_axi_arqos), - .s_axi_arregion (s_axi_arregion), - .s_axi_aruser (s_axi_aruser), - .s_axi_arvalid (s_axi_arvalid), - .s_axi_arready (s_axi_arready), - .s_axi_rid (s_axi_rid), - .s_axi_rdata (s_axi_rdata), - .s_axi_rresp (s_axi_rresp), - .s_axi_rlast (s_axi_rlast), - .s_axi_ruser (s_axi_ruser), - .s_axi_rvalid (s_axi_rvalid), - .s_axi_rready (s_axi_rready), - .m_axi (axi_in) - ); - - axi4_loopback #( - .ADDR_W(ADDR_W), - .DATA_W(DATA_W), - .ID_W(ID_W), - .USER_W(USER_W) - ) dut ( + axi4_loopback dut ( .s_axi(axi_in), .m_axi(axi_out) ); - axi4_if_to_flat #( - .ADDR_W(ADDR_W), - .DATA_W(DATA_W), - .ID_W(ID_W), - .USER_W(USER_W) - ) u_if_to_flat ( - .s_axi (axi_out), - .m_axi_awid (m_axi_awid), - .m_axi_awaddr (m_axi_awaddr), - .m_axi_awlen (m_axi_awlen), - .m_axi_awsize (m_axi_awsize), - .m_axi_awburst (m_axi_awburst), - .m_axi_awlock (m_axi_awlock), - .m_axi_awcache (m_axi_awcache), - .m_axi_awprot (m_axi_awprot), - .m_axi_awqos (m_axi_awqos), - .m_axi_awregion (m_axi_awregion), - .m_axi_awuser (m_axi_awuser), - .m_axi_awvalid (m_axi_awvalid), - .m_axi_awready (m_axi_awready), - .m_axi_wdata (m_axi_wdata), - .m_axi_wstrb (m_axi_wstrb), - .m_axi_wlast (m_axi_wlast), - .m_axi_wuser (m_axi_wuser), - .m_axi_wvalid (m_axi_wvalid), - .m_axi_wready (m_axi_wready), - .m_axi_bid (m_axi_bid), - .m_axi_bresp (m_axi_bresp), - .m_axi_buser (m_axi_buser), - .m_axi_bvalid (m_axi_bvalid), - .m_axi_bready (m_axi_bready), - .m_axi_arid (m_axi_arid), - .m_axi_araddr (m_axi_araddr), - .m_axi_arlen (m_axi_arlen), - .m_axi_arsize (m_axi_arsize), - .m_axi_arburst (m_axi_arburst), - .m_axi_arlock (m_axi_arlock), - .m_axi_arcache (m_axi_arcache), - .m_axi_arprot (m_axi_arprot), - .m_axi_arqos (m_axi_arqos), - .m_axi_arregion (m_axi_arregion), - .m_axi_aruser (m_axi_aruser), - .m_axi_arvalid (m_axi_arvalid), - .m_axi_arready (m_axi_arready), - .m_axi_rid (m_axi_rid), - .m_axi_rdata (m_axi_rdata), - .m_axi_rresp (m_axi_rresp), - .m_axi_rlast (m_axi_rlast), - .m_axi_ruser (m_axi_ruser), - .m_axi_rvalid (m_axi_rvalid), - .m_axi_rready (m_axi_rready) - ); - -endmodule +endmodule \ No newline at end of file diff --git a/axi/tb/axi_cocotb_loopback_test/test_axi4_loopback.py b/axi/tb/axi_cocotb_loopback_test/test_axi4_loopback.py index 4514a2d..588484f 100644 --- a/axi/tb/axi_cocotb_loopback_test/test_axi4_loopback.py +++ b/axi/tb/axi_cocotb_loopback_test/test_axi4_loopback.py @@ -3,7 +3,12 @@ import itertools import cocotb from cocotb.clock import Clock from cocotb.triggers import RisingEdge, Timer -from cocotbext.axi import AxiBus, AxiMaster, AxiRam +from cocotbext.axi import AxiMaster, AxiRam + +from cocotbext.axi import AxiMaster, AxiRam +from axi4_proxy import axi4_bus_from_packed_if, install_cocotbext_axi_slice_compat + +install_cocotbext_axi_slice_compat() class TB: @@ -12,21 +17,28 @@ class TB: cocotb.start_soon(Clock(dut.clk, 10, units="ns").start()) - # Forencich-style connection: - # s_axi: cocotb AXI master -> flat_to_if -> compact AXI interface - # m_axi: compact AXI interface -> if_to_flat -> cocotb AXI RAM - self.master = AxiMaster(AxiBus.from_prefix( - dut, "s_axi"), dut.clk, dut.rst) - self.ram = AxiRam(AxiBus.from_prefix(dut, "m_axi"), - dut.clk, dut.rst, size=2**20) + self.master = AxiMaster( + axi4_bus_from_packed_if(dut.axi_in, "s_axi"), + dut.clk, + dut.rst, + ) + + self.ram = AxiRam( + axi4_bus_from_packed_if(dut.axi_out, "m_axi"), + dut.clk, + dut.rst, + size=2**20, + ) async def reset(self): self.dut.rst.setimmediatevalue(0) await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk) + self.dut.rst.value = 1 await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk) + self.dut.rst.value = 0 await RisingEdge(self.dut.clk) await RisingEdge(self.dut.clk) @@ -36,6 +48,13 @@ def cycle_pause(): return itertools.cycle([1, 1, 1, 0]) +@cocotb.test() +async def inspect_axi_if(dut): + dut._log.info("axi_in fields: %s", dir(dut.axi_in)) + dut._log.info("axi_in.req fields: %s", dir(dut.axi_in.req)) + dut._log.info("axi_in.resp fields: %s", dir(dut.axi_in.resp)) + + @cocotb.test() async def run_basic_write_read_test(dut): # simple loopback test..