From 65cef1457470ad137663d8beb01074b594aedcf4 Mon Sep 17 00:00:00 2001 From: Vamsi Vytla Date: Wed, 24 May 2023 13:56:31 -0700 Subject: [PATCH] Add VLAN (802.1q) tag support for all datawidths of LiteethMAC A working ping and UDP on both HW and sim on verilator tun/tap over 2 VLAN tags and IPs. See bench/sim_xgmii_vlan.py Currently adding a VLAN tag sets up a new port on a newly added MACVLANCrossbar, with a new IP in that virtual network. This can be modded in the future upon necessity and add more IP addresses within the network. common.py: Add vlan header related things core/__init__.py: Adds a separate instantiator for VLANs LiteEthVLANUDPIPCore core/{arp/ip}.py: Facilitate the necessary VLAN related modifications mac/common.py: Update the get_port method of the crossbar to support a fancier dispatcher liteeth/packet.py: VLAN header is 4 bytes, this is smaller than 8 byte dw of xgmii, so necessary changes to handle packet headers that are less than datawidth are in here. Note that header_words == 0 codepath is basically independent from the previous code, and the new packet.py is backwards compatible. --- bench/sim_xgmii_vlan.py | 128 +++++++++++++++++++++++++++++++++++++++ liteeth/common.py | 24 +++++++- liteeth/core/__init__.py | 58 ++++++++++++++++++ liteeth/core/arp.py | 31 +++++++--- liteeth/core/ip.py | 23 +++++-- liteeth/crossbar.py | 6 +- liteeth/mac/common.py | 48 +++++++++++++++ liteeth/packet.py | 68 ++++++++++++++------- 8 files changed, 347 insertions(+), 39 deletions(-) create mode 100644 bench/sim_xgmii_vlan.py diff --git a/bench/sim_xgmii_vlan.py b/bench/sim_xgmii_vlan.py new file mode 100644 index 00000000..95956305 --- /dev/null +++ b/bench/sim_xgmii_vlan.py @@ -0,0 +1,128 @@ +from liteeth.phy.xgmii import LiteEthPHYXGMII +from liteeth.core import LiteEthVLANUDPIPCore +from liteeth.common import convert_ip +from litex.tools.litex_sim import * + + +class VLANSim(SimSoC): + def add_tx_test(self, cd, udp_port, dst_ip, xg_counter, dw=64, always_xmit=True): + send_pkt = Signal(reset=0) + if always_xmit: + send_pkt_counter_d = Signal() + cd += [ + send_pkt_counter_d.eq(xg_counter[18]), + send_pkt.eq(send_pkt_counter_d ^ xg_counter[18]) + ] + + bytes_per_word = dw // 8 + sink_counter = Signal(16) + SINK_LENGTH = 8 * bytes_per_word # 8 words + shift = log2_int(bytes_per_word) # bits required to represent bytes per word + words_per_packet = SINK_LENGTH >> shift + # Note the clkmgt domain + cd += [ + If(send_pkt, + sink_counter.eq(words_per_packet)), + If((sink_counter > 0) & (udp_port.sink.ready == 1), + sink_counter.eq(sink_counter - 1) + ).Else( + udp_port.sink.valid.eq(0), + udp_port.sink.last.eq(0) + ), + udp_port.sink.valid.eq(sink_counter > 0), + udp_port.sink.last.eq(sink_counter == 1), + If(sink_counter == 1, + udp_port.sink.last_be.eq(0x80) + ).Else( + udp_port.sink.last_be.eq(0x0) + ) + ] + + self.comb += [ + # param + udp_port.sink.src_port.eq(3000), + udp_port.sink.dst_port.eq(7778), + udp_port.sink.ip_address.eq(convert_ip(dst_ip)), + udp_port.sink.length.eq(SINK_LENGTH), + + # payload + udp_port.sink.data.eq(Cat(0xc0ffeec1ffee, sink_counter)), + udp_port.sink.error.eq(0) + ] + + def __init__(self, phy_model, host_ip="192.168.2.100", host_udp_port=2000, **soc_kwargs): + SimSoC.__init__(self, + cpu_type = None, + integrated_rom_size = 0x10000, + uart_name = "sim", + with_sdram = False, + with_ethernet = False, + with_etherbone = False, + etherbone_mac_address = 0x10e2d5000001, + etherbone_ip_address = "192.168.2.50", + sdram_module = "MT48LC16M16", + sdram_data_width = 8, + with_sdcard = False, + ) + + DW = 64 if phy_model == "xgmii" else 8 + if DW == 64: + self.submodules.ethphy = LiteEthPHYXGMII(None, self.platform.request("xgmii_eth", 0), model=True) + else: + self.submodules.ethphy = LiteEthPHYGMII(None, self.platform.request("gmii_eth", 0), model=True) + self.submodules.udp_core = LiteEthVLANUDPIPCore(self.ethphy, + 0x10e2d5000001, + convert_ip("192.168.2.50"), + self.sys_clk_freq, + with_ip_broadcast=False, + dw=DW) + udp_core = self.udp_core.add_vlan(vlan_ip="192.168.3.50", vlan_id=2001) + udp_port0 = udp_core.crossbar.get_port(3000, DW) + counter = Signal(28) + self.sync += counter.eq(counter+1) + self.add_tx_test(self.sync, udp_port0, "192.168.3.100", counter, dw=DW) + + udp_core = self.udp_core.add_vlan(vlan_ip="192.168.4.50", vlan_id=2002) + udp_port1 = udp_core.crossbar.get_port(3000, DW) + self.add_tx_test(self.sync, udp_port1, "192.168.4.100", counter, dw=DW) + + +def main(): + from litex.soc.integration.soc import LiteXSoCArgumentParser + parser = LiteXSoCArgumentParser(description="LiteX SoC Simulation utility") + parser.set_platform(SimPlatform) + sim_args(parser) + args = parser.parse_args() + + soc_kwargs = soc_core_argdict(args) + + sys_clk_freq = int(1e6) + sim_config = SimConfig() + sim_config.add_clocker("sys_clk", freq_hz=sys_clk_freq) + sim_config.add_module("serial2console", "serial") + if args.ethernet_phy_model == "xgmii": + sim_config.add_module("xgmii_ethernet", "xgmii_eth", args={"interface": "tap0", "ip": "192.168.2.100"}) + elif args.ethernet_phy_model == "gmii": + sim_config.add_module("gmii_ethernet", "gmii_eth", args={"interface": "tap0", "ip": "192.168.2.100"}) + + # SoC ------------------------------------------------------------------------------------------ + soc = VLANSim(args.ethernet_phy_model, **soc_kwargs) + + def pre_run_callback(vns): + if args.trace: + generate_gtkw_savefile(builder, vns, args.trace_fst) + + # Build/Run ------------------------------------------------------------------------------------ + builder = Builder(soc, **parser.builder_argdict) + builder.build(sim_config=sim_config, + interactive = not args.non_interactive, + pre_run_callback = pre_run_callback, + **parser.toolchain_argdict, + ) + + +# Allows you to test on Debian like system: +# sudo ip link add link tap0 name tap0.2001 type vlan id 2001 && sudo ip addr add 192.168.3.100/24 dev tap0.2001 && sudo ip link set dev tap0.2001 up && sudo ip link add link tap0 name tap0.2002 type vlan id 2002 && sudo ip addr add 192.168.4.100/24 dev tap0.2002 && sudo ip link set dev tap0.2002 up && ip a && sudo tcpdump -i tap0 + +if __name__ == "__main__": + main() diff --git a/liteeth/common.py b/liteeth/common.py index f70a97ed..fe71f7a2 100644 --- a/liteeth/common.py +++ b/liteeth/common.py @@ -25,8 +25,9 @@ eth_preamble = 0xd555555555555555 buffer_depth = 2**log2_int(eth_mtu, need_pow2=False) -ethernet_type_ip = 0x800 -ethernet_type_arp = 0x806 +ethernet_type_ip = 0x0800 +ethernet_type_arp = 0x0806 +ethernet_8021q_tpid = 0x8100 # MAC Constants/Header ----------------------------------------------------------------------------- @@ -38,6 +39,14 @@ } mac_header = Header(mac_header_fields, mac_header_length, swap_field_bytes=True) +vlan_mac_header_length = 4 +vlan_mac_header_fields = { + "vid": HeaderField(0, 0, 16), + "ethernet_type": HeaderField(2, 0, 16) +} + +vlan_mac_header = Header(vlan_mac_header_fields, vlan_mac_header_length, swap_field_bytes=True) + # ARP Constants/Header ----------------------------------------------------------------------------- arp_hwtype_ethernet = 0x0001 @@ -59,6 +68,7 @@ "target_ip": HeaderField(24, 0, 32) } arp_header = Header(arp_header_fields, arp_header_length, swap_field_bytes=True) +arp_vlan_min_length = eth_min_frame_length - eth_fcs_length - mac_header_length - vlan_mac_header_length # Broadcast Constants ------------------------------------------------------------------------------ @@ -184,6 +194,16 @@ def eth_mac_description(dw): ] return EndpointDescription(payload_layout) +def eth_mac_vlan_description(dw): + payload_layout = vlan_mac_header.get_layout() + [ + ("target_mac", 48), + ("sender_mac", 48), + ("data", dw), + ("last_be", dw//8), + ("error", dw//8) + ] + return EndpointDescription(payload_layout) + # ARP def eth_arp_description(dw): param_layout = arp_header.get_layout() diff --git a/liteeth/core/__init__.py b/liteeth/core/__init__.py index 772c76a7..f05e3a0b 100644 --- a/liteeth/core/__init__.py +++ b/liteeth/core/__init__.py @@ -10,6 +10,7 @@ from liteeth.core.ip import LiteEthIP from liteeth.core.udp import LiteEthUDP from liteeth.core.icmp import LiteEthICMP +from liteeth.mac.common import LiteEthMACVLANCrossbar, LiteEthMACVLANPacketizer, LiteEthMACVLANDepacketizer # IP Core ------------------------------------------------------------------------------------------ @@ -61,6 +62,63 @@ def __init__(self, phy, mac_address, ip_address, clk_freq, dw=8, dw = dw, ) +# VLAN UDP IP Core --------------------------------------------------------------------------------- + +class LiteEthVLANUDPIPCore(Module, AutoCSR): + def __init__(self, phy, mac_address, ip_address, clk_freq, + with_icmp=True, + with_ip_broadcast = True, + dw=8): + self.mac_address = mac_address + self.with_icmp = with_icmp + self.with_ip_broadcast = with_ip_broadcast + self.clk_freq = clk_freq + self.dw = dw + ip_address = convert_ip(ip_address) + self.submodules.mac = LiteEthMAC(phy, dw, interface="crossbar", with_preamble_crc=True) + + self.submodules.arp = LiteEthARP(self.mac, mac_address, ip_address, clk_freq, dw=dw) + self.submodules.ip = LiteEthIP(self.mac, mac_address, ip_address, self.arp.table, + with_broadcast=self.with_ip_broadcast, + dw=dw) + + if with_icmp: + self.submodules.icmp = LiteEthICMP(self.ip, ip_address, dw=dw) + + self.submodules.udp = LiteEthUDP(self.ip, ip_address, dw=dw) + + vlan_mac_port = self.mac.crossbar.get_port(ethernet_8021q_tpid, dw=dw) + + self.submodules.crossbar = LiteEthMACVLANCrossbar(dw) + self.submodules.packetizer = LiteEthMACVLANPacketizer(dw) + self.submodules.depacketizer = LiteEthMACVLANDepacketizer(dw) + + self.comb += [ + vlan_mac_port.sink.ethernet_type.eq(ethernet_8021q_tpid), + self.crossbar.master.source.connect(self.packetizer.sink), + self.packetizer.source.target_mac.eq(self.packetizer.sink.target_mac), + self.packetizer.source.sender_mac.eq(self.packetizer.sink.sender_mac), + self.packetizer.source.connect(vlan_mac_port.sink, omit={'ethernet_type'}), + vlan_mac_port.source.connect(self.depacketizer.sink), + self.depacketizer.source.connect(self.crossbar.master.sink), + ] + + def add_vlan(self, vlan_ip="192.168.3.50", vlan_id=2001): + vlan_ip_address = convert_ip(vlan_ip) + arp = LiteEthARP(self, self.mac_address, vlan_ip_address, + self.clk_freq, dw=self.dw, vlan_id=vlan_id) + setattr(self.submodules, f"vlan_{vlan_id}_arp", arp) + ip = LiteEthIP(self, self.mac_address, vlan_ip_address, + arp.table, dw=self.dw, with_broadcast=self.with_ip_broadcast, vlan_id=vlan_id) + setattr(self.submodules, f"vlan_{vlan_id}_ip", ip) + if self.with_icmp: + icmp = LiteEthICMP(ip, vlan_ip_address, dw=self.dw) + setattr(self.submodules, f"vlan_{vlan_id}_ip", icmp) + + udp = LiteEthUDP(ip, vlan_ip_address, dw=self.dw) + setattr(self.submodules, f"vlan_{vlan_id}_udp", udp) + return udp + # UDP IP Core -------------------------------------------------------------------------------------- class LiteEthUDPIPCore(LiteEthIPCore): diff --git a/liteeth/core/arp.py b/liteeth/core/arp.py index 18ccfbc2..b811296c 100644 --- a/liteeth/core/arp.py +++ b/liteeth/core/arp.py @@ -30,13 +30,16 @@ def __init__(self, dw=8): class LiteEthARPTX(Module): - def __init__(self, mac_address, ip_address, dw=8): + def __init__(self, mac_address, ip_address, dw=8, vlan_id=False): self.sink = sink = stream.Endpoint(_arp_table_layout) self.source = source = stream.Endpoint(eth_mac_description(dw)) # # # - packet_length = max(arp_header.length, arp_min_length) + if vlan_id: + packet_length = max(arp_header.length, arp_vlan_min_length) + else: + packet_length = max(arp_header.length, arp_min_length) packet_words = packet_length//(dw//8) counter = Signal(max=packet_words, reset_less=True) @@ -294,16 +297,26 @@ def __init__(self, clk_freq, max_requests=8): # ARP ---------------------------------------------------------------------------------------------- class LiteEthARP(Module): - def __init__(self, mac, mac_address, ip_address, clk_freq, dw=8): - self.submodules.tx = tx = LiteEthARPTX(mac_address, ip_address, dw) + def __init__(self, mac, mac_address, ip_address, clk_freq, dw=8, vlan_id=False): + self.submodules.tx = tx = LiteEthARPTX(mac_address, ip_address, dw, vlan_id=vlan_id) self.submodules.rx = rx = LiteEthARPRX(mac_address, ip_address, dw) self.submodules.table = table = LiteEthARPTable(clk_freq) self.comb += [ rx.source.connect(table.sink), table.source.connect(tx.sink) ] - mac_port = mac.crossbar.get_port(ethernet_type_arp, dw=dw) - self.comb += [ - tx.source.connect(mac_port.sink), - mac_port.source.connect(rx.sink) - ] + if vlan_id: + assert(type(vlan_id) is int) + mac_port = mac.crossbar.get_port((vlan_id << 16) | ethernet_type_arp, dw=dw) + self.comb += [ + mac_port.sink.vid.eq(vlan_id), + tx.source.connect(mac_port.sink), + mac_port.source.connect(rx.sink, omit={'dei', 'pcp', 'vid'}) + ] + + else: + mac_port = mac.crossbar.get_port(ethernet_type_arp, dw=dw) + self.comb += [ + tx.source.connect(mac_port.sink), + mac_port.source.connect(rx.sink) + ] diff --git a/liteeth/core/ip.py b/liteeth/core/ip.py index e29cde81..c2c30b63 100644 --- a/liteeth/core/ip.py +++ b/liteeth/core/ip.py @@ -254,14 +254,25 @@ def __init__(self, mac_address, ip_address, with_broadcast=True, dw=8): # IP ----------------------------------------------------------------------------------------------- class LiteEthIP(Module): - def __init__(self, mac, mac_address, ip_address, arp_table, with_broadcast=True, dw=8): + def __init__(self, mac, mac_address, ip_address, arp_table, with_broadcast=True, dw=8, vlan_id=False): self.submodules.tx = tx = LiteEthIPTX(mac_address, ip_address, arp_table, dw=dw) self.submodules.rx = rx = LiteEthIPRX(mac_address, ip_address, with_broadcast, dw=dw) - mac_port = mac.crossbar.get_port(ethernet_type_ip, dw) - self.comb += [ - tx.source.connect(mac_port.sink), - mac_port.source.connect(rx.sink) - ] + + if vlan_id: + assert(type(vlan_id) is int) + mac_port = mac.crossbar.get_port((vlan_id << 16) | ethernet_type_ip, dw=dw) + self.comb += [ + mac_port.sink.vid.eq(vlan_id), + tx.source.connect(mac_port.sink), + mac_port.source.connect(rx.sink, omit={'dei', 'pcp', 'vid'}) + ] + else: + mac_port = mac.crossbar.get_port(ethernet_type_ip, dw) + self.comb += [ + tx.source.connect(mac_port.sink), + mac_port.source.connect(rx.sink) + ] + self.submodules.crossbar = crossbar = LiteEthIPV4Crossbar(dw) self.comb += [ crossbar.master.source.connect(tx.sink), diff --git a/liteeth/crossbar.py b/liteeth/crossbar.py index 77727493..5e84dcb8 100644 --- a/liteeth/crossbar.py +++ b/liteeth/crossbar.py @@ -30,6 +30,10 @@ def do_finalize(self): # RX dispatch sources = [port.source for port in self.users.values()] self.submodules.dispatcher = Dispatcher(self.master.sink, sources, one_hot=True) - dispatch_sig = getattr(self.master.sink, self.dispatch_param) + if type(self.dispatch_param) is list: + params = [getattr(self.master.sink, param) for param in self.dispatch_param] + dispatch_sig = Cat(*params) + else: + dispatch_sig = getattr(self.master.sink, self.dispatch_param) for i, (k, v) in enumerate(self.users.items()): self.comb += If(dispatch_sig == k, self.dispatcher.sel.eq(2**i)) diff --git a/liteeth/mac/common.py b/liteeth/mac/common.py index b0160d94..11027664 100644 --- a/liteeth/mac/common.py +++ b/liteeth/mac/common.py @@ -26,6 +26,23 @@ def __init__(self, dw): eth_phy_description(dw), mac_header) + +class LiteEthMACVLANDepacketizer(Depacketizer): + def __init__(self, dw): + Depacketizer.__init__(self, + eth_mac_description(dw), + eth_mac_vlan_description(dw), + vlan_mac_header) + + +class LiteEthMACVLANPacketizer(Packetizer): + def __init__(self, dw): + Packetizer.__init__(self, + eth_mac_vlan_description(dw), + eth_mac_description(dw), + vlan_mac_header) + + # MAC Ports ---------------------------------------------------------------------------------------- class LiteEthMACMasterPort: @@ -55,3 +72,34 @@ def get_port(self, ethernet_type, dw=8): raise ValueError("Ethernet type {0:#x} already assigned".format(ethernet_type)) self.users[ethernet_type] = port return port + + +# VLAN MAC Ports ----------------------------------------------------------------------------------- + +class LiteEthMACVLANMasterPort: + def __init__(self, dw): + self.source = stream.Endpoint(eth_mac_vlan_description(dw)) + self.sink = stream.Endpoint(eth_mac_vlan_description(dw)) + + +class LiteEthMACVLANSlavePort: + def __init__(self, dw): + self.sink = stream.Endpoint(eth_mac_vlan_description(dw)) + self.source = stream.Endpoint(eth_mac_vlan_description(dw)) + + +class LiteEthMACVLANUserPort(LiteEthMACVLANSlavePort): + def __init__(self, dw): + LiteEthMACVLANSlavePort.__init__(self, dw) + + +class LiteEthMACVLANCrossbar(LiteEthCrossbar): + def __init__(self, dw=8): + LiteEthCrossbar.__init__(self, LiteEthMACVLANMasterPort, ["ethernet_type", "vid"], dw) + + def get_port(self, vid_ethernet_type, dw=8): + port = LiteEthMACVLANUserPort(dw) + if vid_ethernet_type in self.users.keys(): + raise ValueError("Ethernet type {0:#x} already assigned".format(vid_ethernet_type)) + self.users[vid_ethernet_type] = port + return port diff --git a/liteeth/packet.py b/liteeth/packet.py index 17dbd0c6..e8f13e9e 100644 --- a/liteeth/packet.py +++ b/liteeth/packet.py @@ -40,7 +40,7 @@ def __init__(self, sink_description, source_description, header): # Header Encode/Load/Shift. self.comb += header.encode(sink, self.header) self.sync += If(sr_load, sr.eq(self.header)) - if header_words != 1: + if header_words > 1: self.sync += If(sr_shift, sr.eq(sr[data_width:])) source_last_a = Signal() @@ -50,23 +50,35 @@ def __init__(self, sink_description, source_description, header): # FSM. self.submodules.fsm = fsm = FSM(reset_state="IDLE") fsm_from_idle = Signal() + if header_words < 1: + statements = [source.data.eq(Cat(self.header, sink.data[:data_width - header.length*8])), + sr_load.eq(1), + sink.ready.eq(0), + NextValue(fsm_from_idle, 1), + NextState("UNALIGNED-DATA-COPY") + ] + else: + statements = [ + source.valid.eq(1), + source.data.eq(self.header[:data_width]), + If(source.valid & source.ready, + sr_load.eq(1), + NextValue(fsm_from_idle, 1), + If(header_words == 1, + NextState("ALIGNED-DATA-COPY" if aligned else "UNALIGNED-DATA-COPY") + ).Else( + NextState("HEADER-SEND") + ) + ) + ] + fsm.act("IDLE", sink.ready.eq(1), NextValue(count, 1), If(sink.valid, sink.ready.eq(0), - source.valid.eq(1), source_last_a.eq(0), - source.data.eq(self.header[:data_width]), - If(source.valid & source.ready, - sr_load.eq(1), - NextValue(fsm_from_idle, 1), - If(header_words == 1, - NextState("ALIGNED-DATA-COPY" if aligned else "UNALIGNED-DATA-COPY") - ).Else( - NextState("HEADER-SEND") - ) - ) + *statements ) ) fsm.act("HEADER-SEND", @@ -96,7 +108,7 @@ def __init__(self, sink_description, source_description, header): ) ) if not aligned: - header_offset_multiplier = 1 if header_words == 1 else 2 + header_offset_multiplier = 0 if header_words == 0 else 1 if header_words == 1 else 2 self.sync += If(source.valid & source.ready, sink_d.eq(sink)) fsm.act("UNALIGNED-DATA-COPY", source.valid.eq(sink.valid | sink_d.last), @@ -234,7 +246,7 @@ def __init__(self, sink_description, source_description, header): sink_d = stream.Endpoint(sink_description) # Header Shift/Decode. - if (header_words) == 1 and (header_leftover == 0): + if (header_words == 1 and (header_leftover == 0)) or header_words == 0: self.sync += If(sr_shift, sr.eq(sink.data)) else: self.sync += [ @@ -257,7 +269,7 @@ def __init__(self, sink_description, source_description, header): If(sink.valid, sr_shift.eq(1), NextValue(fsm_from_idle, 1), - If(header_words == 1, + If(header_words <= 1, NextState("ALIGNED-DATA-COPY" if aligned else "UNALIGNED-DATA-COPY"), ).Else( NextState("HEADER-RECEIVE") @@ -288,6 +300,25 @@ def __init__(self, sink_description, source_description, header): ) if not aligned: + if header_words == 0: + statements = [ + source.valid.eq(sink.valid), + sink.ready.eq(~sink_d.last), + If(sink.valid, + NextValue(fsm_from_idle, 0), + sr_shift_leftover.eq(0), + ) + ] + else: + statements = [ + source.valid.eq(sink_d.last), + sink.ready.eq(~sink_d.last), + If(sink.valid, + NextValue(fsm_from_idle, 0), + sr_shift_leftover.eq(1), + ) + ] + self.sync += If(sink.valid & sink.ready, sink_d.eq(sink)) fsm.act("UNALIGNED-DATA-COPY", source.valid.eq(sink.valid | sink_d.last), @@ -296,12 +327,7 @@ def __init__(self, sink_description, source_description, header): source.data.eq(sink_d.data[header_leftover*8:]), source.data[min((bytes_per_clk-header_leftover)*8, data_width-1):].eq(sink.data), If(fsm_from_idle, - source.valid.eq(sink_d.last), - sink.ready.eq(~sink_d.last), - If(sink.valid, - NextValue(fsm_from_idle, 0), - sr_shift_leftover.eq(1), - ) + *statements ), If(source.valid & source.ready, If(source.last,