lacp: implement IEEE 802.3ad link aggregation

Dynamic link aggregation on top of the static LAGG, for RTL8372/8373:
receive, selection and mux state machines (coupled mux, one aggregator
per group), with "lacp show" to look inside. Each of the four trunk
groups runs its own aggregator on the candidate ports assigned with
"lag <n> lacp <ports>"; the engine starts with the first such group and
stops with the last.

LACPDUs reach the CPU through the reserved-multicast trap, so delivery
needs no L2 entry and nothing written per VLAN or per PVID. The trap
acts on the address before the VLAN lookup and before the ingress
filters, so a LACPDU also arrives on a port that admits tagged frames
only and on a port STP holds in blocking, neither of which a forwarded
one reaches. A trapped frame carries no VLAN tag,
so lacp_in() takes the offset of the PDU body from TRAP_RX_BODY(). The
action is set with trap_rma_set(), which leaves the flags sharing that
register alone, and "lacp off" puts the slow-protocols drop default
back. doc/lacp.md and doc/link_aggregation.md record what was measured,
including why the trap looked like a dead end at first.

The web UI gets a Static/LACP mode per group on the LAG page with live
status, and the startup-config validator knows the per-group syntax. A
host-side sandbox for the state machines runs in the test harness
together with the LAG hash check. It drives both forms of an incoming
LACPDU, so the body offset of a trapped one is covered as well.

The L2 table page iterated with the "next unicast" read method, so
static multicast entries such as an IGMP group were invisible. It now
iterates with "next address", skips L3 entries (their layout carries IP
addresses where a MAC is expected), and reports the lowest member of a
multicast entry's port mask, so such an entry shows up as a static
entry on that port.

A port taken out of LACP forgets its partner, so "lacp show" after
"lacp off" no longer reports one. Applying the same candidate set to a
group again, as the LAG page does on every Apply, leaves the trunk
alone instead of tearing it down for a second or two. lacp_in() checks
the frame length and ethertype before reading a LACPDU, so a runt or a
foreign frame on the slow-protocols address cannot leave stale buffer
bytes as a partner. The sandbox covers these three in T20-T22. On the
LAG page, LACP mode with no ports checked sends "lag N lacp off", and
the startup-config validator wants at least one port after "lacp".
This commit is contained in:
DrDoof authored and d00f committed 2026-10-12 15:09:57 +02:00
1 parent b83b67d234
commit c3b4cc893b
31 files changed
+1885 -23

No files matched your search

+1
View File
@@ -90,6 +90,7 @@ SRCS += \
rtl837x_stp.c \
rtl837x_lldp.c \
rtl837x_storm.c \
rtl837x_lacp.c \
rtl837x_trap.c
SRCS += \
httpd/httpd.c \
+1
View File
@@ -359,6 +359,7 @@ The following documents give further documentation on specific features of the R
- [IGMP (IP-MC streaming)](doc/igmp.md)
- [SFP+ ports](doc/sfp.md)
- [Trunking aka. port aggregation](doc/trunking.md)
- [LACP (802.3ad link aggregation)](doc/lacp.md)
- [VLAN](doc/vlan.md)
- [Storm control](doc/storm_control.md)
- [Modifications and Flash replacement](doc/mods.md)
+26 -3
View File
@@ -14,6 +14,7 @@
#include "rtl837x_regs.h"
#include "rtl837x_sfr.h"
#include "rtl837x_stp.h"
#include "rtl837x_lacp.h"
#include "rtl837x_igmp.h"
#include "rtl837x_bandwidth.h"
#include "rtl837x_lldp.h"
@@ -537,7 +538,18 @@ void parse_lag(void)
return;
}
/* "lag <n> lacp [port]..." - run LACP on this LAG with the given candidate
* ports; "lag <n> lacp off" (or no ports) removes the LAG from LACP. */
uint8_t w = 2;
__xdata uint8_t lacp_mode = 0;
if (cmd_words_len > 2 && cmd_compare(2, "lacp")) {
lacp_mode = 1;
w = 3;
if (cmd_words_len > 3 && cmd_compare(3, "off")) {
lacp_lag_set(group, 0);
return;
}
}
while (w < cmd_words_len) {
// Parse port.
if (cmd_parse_port_separator(cmd_words_b[w++]) == 0)
@@ -545,10 +557,13 @@ void parse_lag(void)
members |= ((uint16_t)1) << atoi_results_u8;
}
port_lag_members_set(group, members);
if (lacp_mode)
lacp_lag_set(group, members); /* 0 ports also valid: removes the LAG */
else
port_lag_members_set(group, members);
return;
err:
cmd_error("lag (show | <1-4> (d | <port>...))\n");
cmd_error("lag (show | <1-4> (d | [lacp] <port>...))\n");
}
@@ -1972,11 +1987,19 @@ void cmd_parser(void) __banked
} else if (cmd_compare(0, "health")) {
health_show();
#endif
} else if (cmd_compare(0, "lacp")) {
if (cmd_compare(1, "show"))
lacp_show();
else if (cmd_compare(1, "off"))
lacp_off();
else
cmd_error("lacp (show | off); a group starts LACP with: lag <1-4> lacp <port>...\n");
} else if (cmd_compare(0, "pvid")) {
if (cmd_words_len == 3 && cmd_parse_port_separator(cmd_words_b[1]) != 0
&& atoi_short(cmd_words_b[2]) && atoi_results_short && atoi_results_short <= 4094)
{
port_pvid_set(atoi_results_u8, atoi_results_short);
else {
} else {
cmd_error("pvid <port> <1-4094>\n");
}
} else if (cmd_compare(0, "vlan")) {
+5
View File
@@ -79,6 +79,11 @@ bit7 KEEP | 6 VSEL | 5 LEARN_DIS | 4:0 VIDX
All fields are in network byte order.
This word has to be written through `HTONS` like every other field of the tag.
Writing a constant raw puts the bits in the wrong byte, so `0x0020` reaches the
wire as `0x2000`, which is EFID rather than LEARN_DIS. The ASIC then fails to
parse the tag and forwards the frame with the `0x8899` header still on it.
* `EFID_EN`, `EFID`: look the destination up under this filtering ID
instead of the port's own
* `PRI_EN`, `PRI`: force the given priority on the frame
+231
View File
@@ -0,0 +1,231 @@
# LACP (Link Aggregation Control Protocol, IEEE 802.3ad)
A static link aggregation group works only if both ends agree about it in
advance. Nothing checks that they do: cable a member to the wrong neighbour and
the group keeps hashing frames onto a link that goes somewhere else. LACP is the
protocol that makes the two ends agree, and keeps agreeing — it discovers which
links reach the same partner, aggregates only those, and drops a link out of the
group when the partner stops answering.
The switch runs LACP in software on the 8051. Frames are the Slow Protocols
group: destination `01:80:C2:00:00:02`, ethertype `0x8809`, subtype 1. The
implementation covers the parts that decide whether an aggregate forms:
the Receive machine and the Selection and Mux machines of 802.3ad clause 43,
`recordPDU` and `update_NTT`, and the mis-cabling protection that refuses to
aggregate links whose partners differ. It does not implement the Churn Detection
machines or the Marker protocol, and the Mux machine is the coupled variant, so
collecting and distributing are enabled together rather than in sequence.
Up to `LACP_NUM_LAGS` (4) groups can be placed under LACP; the rest stay static.
## Getting the frames to the CPU
Reserved multicast addresses are handled by the RMA block before the L2 lookup
happens. Each address has its own register and the action field sits in bits
[5:4]: 0 forward, 1 trap to CPU, 2 drop, 3 forward excluding the CPU. The
registers are packed and consecutive by *entry* rather than by address byte, so
the step of 4 counts entries, not bytes of the address: `...:00` sits at
`0x4ecc` and `...:02` at `0x4ed4`, but `...:08` sits at `0x4ee0`.
```
#define RTL837X_RMA0_CONF 0x4ecc /* 01:80:C2:00:00:00, entry 0 */
#define RTL837X_RMA2_CONF 0x4ed4 /* 01:80:C2:00:00:02, Slow Protocols */
#define RTL837X_RMA_ACT_MASK 0x30
#define RTL837X_RMA_ACT_TRAP 0x10
#define RTL837X_RMA_ACT_DROP 0x20
```
The engine puts this address on *trap* while it runs. A trap takes the frame
out of the forwarding path and hands it to the port named in `EXT_CPU_CTRL`
(`0x6724`), whose reset value names no port at all: the destination is meant to
be an *external* CPU on a physical port, which these boards do not have. That
is why the trap first looked like a dead end - the receive counters stayed
frozen while the partner kept sending, and the ingress discard counter grew
instead. `trap_init()` points the register at the CPU port once at startup, and
the same frames then arrive.
`FORWARD` reaches the CPU too, because the CPU port is in the forwarding
domain, but a forwarded frame is subject to the L2 lookup and on its own floods
to every other port in the VLAN. That leaks a neighbour's LACPDUs to hosts that
must not see them and, with two bonds on one switch, poisons both; a reserved
link-local group must not be forwarded at all. Containing it takes a static L2
multicast entry with a CPU-only member mask, and because lookups are IVL on
this chip, one entry per distinct pvid over all front panel ports - a VID 0
entry is never matched, and a port outside the LACP groups needs its pvid
covered as well, or a LACPDU arriving there misses the lookup and is flooded
through its VLAN.
The trap needs none of that and reaches further, because it acts on the address
before the VLAN lookup and before the ingress filters. An untagged LACPDU is
delivered on a port that admits tagged frames only, where a forwarded one is
dropped by the acceptable-frame-type filter; and it is delivered on a port STP
holds in blocking, so a group converges without waiting out the forward delay.
Both were measured against forward on the same ports of an SWTGW218AS.
A trapped frame arrives without the VLAN tag the hardware inserts into a
forwarded one, so the LACPDU body sits four bytes earlier. `TRAP_RX_BODY()`
gives the offset of the body for either form, and `lacp_in()` reads the PDU
from there.
Neighbouring addresses in the same block still differ in other respects -
`...:00` is exempt from the ingress STP-state check and `...:02` is not, which
the trap makes moot only because it acts before that check - so measure before
carrying a conclusion about one of them across to another.
When LACP is switched off the action goes to `DROP`, which acts before the L2
lookup, so there is nothing to undo.
## What has to match across the members
802.3ad requires that every link in an aggregate be full duplex and run at the
same speed. Beyond the standard, anything that changes how a frame is treated
has to be identical across members, because the hash picks the member per frame
and a difference would make the behaviour depend on that choice:
* must match: speed and duplex, VLAN membership and pvid, ingress filtering,
port isolation
* may differ: EEE, LED configuration, counters, cable diagnostics
None of this is enforced yet — `port_lag_members_set()` writes a member mask and
a hash setting and validates nothing. See #377 for the discussion of whether a
mismatch should be rejected or applied to the whole group.
## Protocol timing
```
#define LACP_FAST_PERIODIC 0x0032 /* fast TX 1 s; partner expires at 3 s */
#define LACP_SLOW_PERIODIC 0x05dc /* slow TX 30 s; partner expires at 90 s */
#define LACP_SHORT_TIMEOUT 0x00c8 /* silent partner dropped after 4 s (802.3ad: 3 s) */
#define LACP_LONG_TIMEOUT 0x1770 /* long-timeout variant, after 120 s (802.3ad: 90 s) */
```
One unit is one step of the LACP timers, taken every four system ticks, so 50
per second whatever the pace of the main loop; 50 units came out as 1.00 s on
the wire in all 116 intervals of a two minute capture.
The two transmit periods are worth a word, because they are the one thing here
that is not ours to choose freely. Our own timeouts only decide how patient we
are with a partner that has gone quiet, and being slow there costs nobody
anything. The transmit period is the opposite: it decides when the *partner*
gives up on us. 802.3ad pairs a 1 s fast period with a 3 s short timeout, and a
30 s slow period with a 90 s long timeout, and the ratio is the whole point.
Getting that wrong does not break the aggregate, which is what makes it easy to
miss. An earlier version of this code transmitted every 2 to 3 s while the
partner was asking for the fast rate. The links still aggregated and still
carried traffic, but every time a gap crossed 3 s the partner's receive machine
timed us out and recovered: 13 of its 93 LACPDUs came back carrying `EXPIRED`
and reporting our SYNC as cleared. Since a received actor state is recorded
verbatim as the partner state and sent back out, the flap was visible from both
ends, and the bond's churn machines never settled. With the periods above, the
same capture shows no expiry at all and both churn states read `none`.
Our own timeouts sit one period above the standard's 3 s and 90 s, which
absorbs a single lost LACPDU plus the tick jitter. A partner whose LACP has
stopped while its link stayed up is out of the trunk after that; anything
longer keeps hashing traffic onto a member that no longer agrees to carry it.
## LACP API
```
void lacp_init(void) __banked; /* boot init: clear per-LAG state */
void lacp_off(void) __banked;
void lacp_lag_set(uint8_t lag, uint16_t ports) __banked;
void lacp_in(void) __banked;
void lacp_timers(void) __banked;
void lacp_show(void) __banked;
```
`lacp_init()` has to run before the startup configuration replays, because xdata
is not zeroed and the port-to-LAG map uses `0xff` as "no LAG"; a stray zero would
make a port look like it belongs to LAG 0.
## LACP configuration on the serial console
A group is put under LACP by adding `lacp` to the `lag` command:
```
lag 1 lacp 7 8 # ports 7 and 8 become LACP candidates of group 1
lag 1 lacp off # group 1 goes back to static (so does "lag 1 lacp")
lag 1 7 8 # plain static aggregation, no protocol
```
A group put under LACP gives up the static trunk it had: the hardware trunk is
cleared and rebuilt from the ports that converge. A port named in another LACP
group moves to the new one. The protocol starts with the first group put under
LACP and stops when the last one is taken out; there is no separate switch for
it. Two commands act on
the whole engine:
```
lacp off # every group back to static, protocol stopped
lacp show
```
`lacp show` prints the engine state, then one line per group with its candidate
ports, the elected aggregator and the trunk members actually programmed into the
hardware, then one line per port with the actor and partner state bytes and the
receive-machine state.
## LACP configuration via the Web Interface
Each group on the LAG page has a Static/LACP mode next to its member ports;
for an LACP group it shows the elected aggregator and the active members. Below
the groups a table has one row per LACP port with its group, actor and partner
state, receive state, LACPDU count and partner system. The page reads
`/lacp.json` every three seconds while it is open. The member checkboxes of a
static group come from the hardware registers in `/lag.json`.
## A test with a Linux 802.3ad bond
Two ports of the switch to two interfaces of one host:
```
ip link add bond0 type bond mode 802.3ad
ip link set enp1s0 down && ip link set enp1s0 master bond0
ip link set enp2s0 down && ip link set enp2s0 master bond0
ip link set bond0 up
```
On the switch, with those two ports as the group:
```
lag 1 lacp 7 8
```
`lacp show` should then give both ports an actor state of `0x3f`, which is
ACTIVITY, TIMEOUT, AGGREGATION, SYNC, COLLECTING and DISTRIBUTING together, a
partner state with SYNC in it, and the partner's system MAC. `lag show` should
list both ports as trunk members, since that reads the hardware rather than the
protocol's opinion of itself.
The host's view is the one worth trusting, in `/proc/net/bonding/bond0`. Both
slaves want the **same `Aggregator ID`**; two different numbers mean the bond
has not aggregated anything and each link is sitting in its own group, which
looks healthy at a glance and is not. Both churn states should read `none`.
`monitoring` that never settles is the signature of our transmit period being
too slow for the rate the partner asked for.
Three things are worth checking past convergence, because each of them has been
broken here at some point:
* `tcpdump -i enp1s0 ether proto 0x8809` on each slave should show the switch's
LACPDUs and that slave's own, and never the other slave's. A sibling leak is
what the trap prevents: the PDU never enters the forwarding path at all.
* No `illegal loopback` in the host's log.
* In a hex dump of a frame from the switch, the three reserved bytes after the
actor state should be zero. They used to carry whatever the transmit buffer
held from the previous frame.
Pulling one slave down and putting it back is the cheapest failure test. The
group narrows to the surviving member and comes back within tens of seconds,
and traffic across the transition loses a few percent at the edges rather than
half of everything, which is what it would lose if the ASIC kept hashing onto
the dead member.
## Known limits
* No Churn Detection machines and no Marker protocol.
* Coupled Mux: collecting and distributing are enabled together.
* One aggregator per group.
* Member compatibility is not validated; see #377.
+49
View File
@@ -227,3 +227,52 @@ iperf Done.
```
As you can see, the total throughput was 4.71 GBit/sec which is close to the
maximum possible with a single 5GBit link.
## LACP: how the PDUs reach the CPU
Slow-protocol frames go to the reserved group `01:80:C2:00:00:02`, and the
ASIC's default action for it is *drop*. The engine sets the action to *trap*
while it runs. A trap hands the frame to the port named in `EXT_CPU_CTRL`,
which names no port out of reset - that is why the trap first looked like no
help at all: the receive counters stayed frozen while the partner kept sending,
and the frames were counted as ingress discards. `trap_init()` points the
register at the CPU port once at startup.
The *forward* action would reach the CPU as well, since the CPU port is in the
forwarding domain, but it leaves the frame in the L2 lookup and so floods it
across the ingress VLAN. That leaks LACPDUs to unrelated ports, and the damage
is real: a bond's own ports see each other's PDUs and Linux reports an illegal
loopback, a second bond on the same switch is poisoned, and the flood escapes
through uplinks. Containing it would take a static L2 multicast entry with a
CPU-only mask for every PVID in use, since untagged LACPDUs classify into the
ingress port's PVID and the forward action is not limited to the LACP ports.
The trap keys on the address instead, before the VLAN lookup and before the
ingress filters, which settles three things at once:
* Nothing is written per VLAN or per PVID, so delivery no longer depends on
which PVIDs the ports happen to carry.
* A port that admits tagged frames only still receives: an untagged forwarded
LACPDU is dropped by the acceptable-frame-type filter, a trapped one is not.
* A port STP holds in blocking still receives, so a group converges without
waiting out the forward delay.
A trapped frame arrives without the VLAN tag the hardware inserts into a
forwarded one, so the PDU body sits four bytes earlier; `TRAP_RX_BODY()` gives
the offset for both forms.
`lacp off` puts the action back to *drop*, which acts before the L2 lookup.
## LACP: what the implementation leaves out
The four state machines of Clause 43 are present in simplified form. Mux control
is coupled rather than independent, one partner system is elected per LAG, and
there are no churn detection machines.
One interoperability rule is worth stating because getting it wrong is silent.
The Partner block of an outgoing PDU has to echo the peer's own actor identity
verbatim, meaning system and priority, key, port and port priority, and the
aggregation flag. A Linux partner's `__record_pdu()` accepts our SYNC bit only
when that block mirrors what it sent. Hardcoding the priorities to zero
mismatches Linux's default of 0xffff, which clears partner SYNC, moves the state
byte from 0x3f to 0x37, and the bond never reaches collecting and distributing.
+44 -8
View File
@@ -67,6 +67,9 @@ iso_title:"Port isolation",iso_from:"From port",iso_to:"To port",iso_all:"Allow
iso_intro:"A frame entering a port may only leave through the ports ticked in its row. Isolation is decided before forwarding, so a port that is not ticked is unreachable directly, whatever the address table says.",
iso_legend:"The cell of the port itself is always allowed. A port number in red marks a port that is not fully open.",
lag_hash:"Hash:",lag_note:"A LAG needs at least one member to be saved in the startup config; applying an empty group clears it.",
lacp_heading:"LACP (802.3ad)",lag_mode:"Mode",lag_static:"Static",lacp_neg:"negotiating",lacp_agg:"aggregator",
lacp_members:"active members",lacp_hint:"LACP: select candidate ports and press Apply to start negotiation",
lacp_actor:"Actor",lacp_partner:"Partner",lacp_rxstate:"RX state",lacp_rx:"LACPDUs received",lacp_psys:"Partner system",
lag_clear_q:"Clear LAG {n}?",lag_clear_d:"All member ports return to normal operation.",
e_title:"Energy Efficient Ethernet",e_adv:"Advertised",e_lp:"Link partner",e_active:"Active",e_enable:"Enable",
e_note:"Advertised and link-partner flags per speed: 100M, 1G, 2.5G. SFP ports do not support EEE.",
@@ -808,7 +811,7 @@ var CONF_CMDS=[
/^port\s+\d{1,2}\s+lldp\s+(permit|block)+$/,
/^eee\s+(on|off)(\s+\d{1,2})?(\s+(100m|1g|2g5))?$/,
/^mirror(\s+\d{1,2})(\s+\d{1,2}[tr]?)+$/,/^mirror\s+off$/,
/^lag\s+[1-4](\s+\d{1,2})+$/,/^lag\s+[1-4]\s+d$/,/^laghash\s+[1-4](\s+\w+)+$/,
/^lag\s+[1-4](\s+\d{1,2})+$/,/^lag\s+[1-4]\s+d$/,/^lag\s+[1-4]\s+lacp(\s+\d{1,2})+$/,/^lag\s+[1-4]\s+lacp\s+off$/,/^laghash\s+[1-4](\s+\w+)+$/,
/^isolate\s+\d{1,2}(\s+(off|\d{1,2}))+$/,
/^stp\s+(on|off)$/,/^stp\s+(prio|hello|maxage|fwd|txhold)\s+\d{1,2}$/,
/^stp\s+version\s+(rstp|stp)$/,
@@ -1814,6 +1817,15 @@ function buildLag(){
for(var g=1;g<=4;g++)(function(g){
var card=h("div",{class:"card"});
card.appendChild(h("h2",{text:"LAG "+g}));
var mr=h("div",{style:"display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:10px",class:"small"});
mr.appendChild(h("span",{class:"mut",text:t("lag_mode")}));
var ms=h("select",{class:"in",id:"lgm"+g});
ms.appendChild(h("option",{value:"static",text:t("lag_static")}));
ms.appendChild(h("option",{value:"lacp",text:"LACP"}));
ms.addEventListener("change",function(){$("lgs"+g).textContent=ms.value==="lacp"?t("lacp_hint"):""});
mr.appendChild(ms);
card.appendChild(mr);
card.appendChild(h("p",{class:"small mono",id:"lgs"+g,style:"margin-bottom:10px"}));
var pr=h("div",{style:"display:flex;gap:10px;flex-wrap:wrap;margin-bottom:10px"});
for(var p=1;p<=S.n;p++)pr.appendChild(h("label",null,[
h("input",{type:"checkbox",id:"lg"+g+"p"+p}),document.createTextNode(" "+p+" "),
@@ -1826,14 +1838,33 @@ function buildLag(){
});
card.appendChild(hr);
card.appendChild(h("button",{class:"ctl pri",text:t("c_apply"),onclick:function(){lagApply(g)}}));
card.addEventListener("change",function(){card.dataset.dirty="1"});
w.appendChild(card);
})(g);
}
var lacpCfg=[0,0,0,0,0];
function lagLoad(){
return getJSON("/lag.json").then(function(s){
return getJSON("/lacp.json").then(function(c){
var tb=$("lacptbl").tBodies[0];
tb.innerHTML="";
c.lags.forEach(function(lg,i){
var g=i+1;
lacpCfg[g]=parseInt(lg.cfg,16);
if($("lgm"+g).closest(".card").dataset.dirty)return;
$("lgm"+g).value=lacpCfg[g]?"lacp":"static";
$("lgs"+g).textContent=lacpCfg[g]?"LACP: "+t("lacp_agg")+" "+(lg.aggValid?lg.agg:"("+t("lacp_neg")+")")+", "+t("lacp_members")+" 0x"+lg.members:"";
});
if(c.on)c.ports.forEach(function(p){
if(p.lag===255)return;
var tr=tb.insertRow();
[p.p,p.lag+1,p.a,p.pt,p.rs,parseInt(p.rx,16),p.psys].forEach(function(v){tr.insertCell().textContent=v});
});
return getJSON("/lag.json");
}).then(function(s){
s.forEach(function(l){
var g=l.lagNum+1;
var members=parseInt(l.members,2);
if($("lgm"+g).closest(".card").dataset.dirty)return;
var members=lacpCfg[g]||parseInt(l.members,2);
for(var p=1;p<=S.n;p++)
$("lg"+g+"p"+p).checked=!!((members>>S.physToLog[p-1])&1);
var hash=parseInt(l.hash,16);
@@ -1842,21 +1873,25 @@ function lagLoad(){
});
}
function lagApply(g){
var cmd="lag "+g,n=0;
var lacp=$("lgm"+g).value==="lacp",cmd="lag "+g+(lacp?" lacp":""),n=0,pre=[];
delete $("lgm"+g).closest(".card").dataset.dirty;
for(var p=1;p<=S.n;p++)if($("lg"+g+"p"+p).checked){cmd+=" "+p;n++;}
if(!n){
if(!lacp&&lacpCfg[g])pre.push("lag "+g+" lacp off");
if(lacp&&!n)cmd="lag "+g+" lacp off";
if(!n&&!lacp){
confirmModal(t("lag_clear_q",{n:g}),t("lag_clear_d"),function(){
postCmd("lag "+g+" d").then(lagLoad).catch(function(){});
postCmds(pre.concat(["lag "+g+" d"])).then(lagLoad).catch(function(){});
});
return;
}
var hcmd="laghash "+g,nh=0;
HASHF.forEach(function(f){if($("lg"+g+"h"+f).checked){hcmd+=" "+f;nh++;}});
var cmds=[cmd];
var cmds=pre.concat([cmd]);
if(nh)cmds.push(hcmd);
postCmds(cmds).then(lagLoad).catch(function(){});
}
tabHooks.lag={enter:function(){needPorts(function(){buildLag();lagLoad().catch(function(){})})}};
var lagPoller=new Poller(function(){return lagLoad().catch(function(){})},3000);
tabHooks.lag={enter:function(){needPorts(function(){buildLag();lagPoller.start()})},leave:function(){lagPoller.stop()}};
function lldpLoad(){
@@ -2245,6 +2280,7 @@ function mergeConf(base,texts){
else drop(/^eee /);
conf.push(line);return;
}
if((m=line.match(/^lag (\d) lacp off$/))){drop(new RegExp("^lag "+m[1]+" lacp( |$)"));return;}
if(line==="mirror off"){drop(/^mirror /);return;}
if(!isConfCmd(line))return;
if((m=line.match(/^ingress (.+)$/))){
+5
View File
@@ -356,6 +356,11 @@ button.danger{background:var(--bad);color:#fff;border-color:transparent}
<section class="tab" id="tab-lag">
<div id="lagwrap" class="grid2"></div>
<div class="card" style="margin-top:14px"><h2 data-i18n="lacp_heading">LACP (802.3ad)</h2>
<div class="scrollx"><table class="t" id="lacptbl">
<thead><tr><th data-i18n="c_port">Port</th><th>LAG</th><th data-i18n="lacp_actor">Actor</th><th data-i18n="lacp_partner">Partner</th><th data-i18n="lacp_rxstate">RX state</th><th data-i18n="lacp_rx">LACPDUs received</th><th data-i18n="lacp_psys">Partner system</th></tr></thead>
<tbody></tbody></table></div>
</div>
<p class="small mut" style="margin-top:10px" data-i18n="lag_note">A LAG needs at least one member to be saved in the startup config; applying an empty group clears it.</p>
</section>
+2
View File
@@ -1036,6 +1036,8 @@ void httpd_appcall(void)
send_stp();
} else if (is_word(q, "/lldp.json")) {
send_lldp();
} else if (is_word(q, "/lacp.json")) {
send_lacp();
} else if (is_word(q, "/vlanlist")) {
send_vlanlist();
} else if (is_word(q, "/config")) {
+77 -4
View File
@@ -14,6 +14,7 @@
#include "version.h"
#include "machine.h"
#include "rtl837x_stp.h"
#include "rtl837x_lacp.h"
#include "page_impl.h"
#include "syslog.h"
@@ -408,14 +409,19 @@ void send_l2(uint16_t idx)
*/
__xdata uint16_t entry = idx & 0xfff;
__xdata uint16_t first_entry = 0xffff; // An illegal entry index
__xdata uint16_t mbr;
__bit first = true;
char_to_html('[');
while (1) {
entries_left--;
uint8_t port = 0;
uint8_t lag;
uint8_t mc = 0;
reg_read_m(RTL837x_TBL_DATA_0);
REG_WRITE(RTL837x_TBL_DATA_0, sfr_data[0], sfr_data[1] & 0xfc, sfr_data[2] | (TBL_LUTREAD_NEXT_L2UC << 6), sfr_data[3]);
// Iterate with "next address" (method 2) so static multicast entries -
// an IGMP group entry, say - are listed too, not only learned or
// static unicast ones.
REG_WRITE(RTL837x_TBL_DATA_0, sfr_data[0], sfr_data[1] & 0xfc, sfr_data[2] | (TBL_LUTREAD_NEXT_ADDRESS << 6), sfr_data[3]);
REG_WRITE(RTL837X_TBL_CTRL, entry >> 8, entry, TBL_L2_UNICAST, TBL_EXECUTE);
do {
@@ -423,7 +429,7 @@ void send_l2(uint16_t idx)
} while (sfr_data[3] & TBL_EXECUTE);
reg_read_m(RTL837x_L2_DATA_OUT_B);
__bit valid = (sfr_data[0] & 0x20) != 0;
__bit valid = (sfr_data[0] & 0x20) && !(sfr_data[0] & 0x10);
if (valid) {
/* separator + 74-byte worst-case entry + closing "]" */
if (slen + 76 > TCP_OUTBUF_SIZE)
@@ -432,6 +438,8 @@ void send_l2(uint16_t idx)
char_to_html(',');
first = false;
mc = sfr_data[2] & 0x01; // multicast MAC: member mask, no SPA/age
// VLAN, taken from the read above instead of reading the register twice
slen += strtox(outbuf + slen, "{\"vlan\":\"");
charhex_to_html(sfr_data[0] & 0x0f);
@@ -450,12 +458,23 @@ void send_l2(uint16_t idx)
// type
reg_read_m(RTL837x_L2_DATA_OUT_C);
if (sfr_data[1] & 0x1)
if (mc || (sfr_data[1] & 0x1))
slen += strtox(outbuf + slen, "\",\"type\":\"s\",\"port\":");
else
slen += strtox(outbuf + slen, "\",\"type\":\"l\",\"port\":");
port |= (sfr_data[3] & 0x3) << 2;
if (mc) {
// Report the lowest member of the mask (an entry holding only
// the CPU port is rendered as "CPU" by the UI)
mbr = port | ((uint16_t)sfr_data[3] << 2);
port = 0;
while (mbr && !(mbr & 1)) {
mbr >>= 1;
port++;
}
} else {
port |= (sfr_data[3] & 0x3) << 2;
}
itoa_html(port);
slen += strtox(outbuf + slen, ",\"lag\":");
lag = port_lag_of(port);
@@ -736,6 +755,60 @@ void send_stp(void)
slen += strtox(outbuf + slen, "]}");
}
/* LACP protocol status for the LAG page ("/lacp.json"). State variables are
* exported read-only via rtl837x_lacp.h; hardware trunk membership itself is
* already visible through send_lag() (it reads the trunk registers). */
void send_lacp(void)
{
dbg_string("send_lacp called\n");
slen = strtox(outbuf, HTTP_RESPONCE_JSON);
slen += strtox(outbuf + slen, "{\"on\":");
bool_to_html(lacpEnabled);
/* Per-LAG section: candidate ports, elected aggregator, trunk members. */
slen += strtox(outbuf + slen, ",\"lags\":[");
for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) {
slen += strtox(outbuf + slen, "{\"cfg\":\"");
byte_to_html(lacp_lag_ports[l] >> 8);
byte_to_html(lacp_lag_ports[l]);
slen += strtox(outbuf + slen, "\",\"aggValid\":");
bool_to_html(lacp_agg_valid[l]);
slen += strtox(outbuf + slen, ",\"agg\":\"");
for (uint8_t j = 0; j < 6; j++)
byte_to_html(lacp_agg_sys[l][j]);
slen += strtox(outbuf + slen, "\",\"members\":\"");
byte_to_html(lacp_members_last[l] >> 8);
byte_to_html(lacp_members_last[l]);
slen += strtox(outbuf + slen, "\"},");
}
slen -= 1; // remove comma
slen += strtox(outbuf + slen, "],\"ports\":[");
for (uint8_t i = machine.min_port; i <= machine.max_port; i++) {
slen += strtox(outbuf + slen, "{\"p\":");
itoa_html(machine.log_to_phys_port[i]);
/* Which LACP LAG this port belongs to; 255 = none */
slen += strtox(outbuf + slen, ",\"lag\":");
itoa_html(lacp_port_lag[i]);
slen += strtox(outbuf + slen, ",\"a\":\"");
byte_to_html(lacp_actor_state[i]);
slen += strtox(outbuf + slen, "\",\"pt\":\"");
byte_to_html(lacp_partner_state[i]);
slen += strtox(outbuf + slen, "\",\"rs\":");
itoa_html(lacp_rx_state[i]);
/* rx as 4-digit hex: itoa16_html() only renders values up to 9999
* correctly (see its VLAN-ID comment), the counter goes to 65535 */
slen += strtox(outbuf + slen, ",\"rx\":\"");
byte_to_html(lacp_rx_count[i] >> 8);
byte_to_html(lacp_rx_count[i]);
slen += strtox(outbuf + slen, "\",\"psys\":\"");
for (uint8_t j = 0; j < 6; j++)
byte_to_html(lacp_partner_sys[i][j]);
slen += strtox(outbuf + slen, "\"},");
}
slen -= 1; // remove comma
slen += strtox(outbuf + slen, "]}");
}
void send_lldp(void)
{
dbg_string("send_lldp called\n");
+1
View File
@@ -24,6 +24,7 @@ void send_cmd_log(void);
void send_lag(void);
void send_stp(void);
void send_lldp(void);
void send_lacp(void);
void send_vlanlist(void);
/* Convert only the lower nibble to ascii HEX char.
+2 -2
View File
@@ -22,8 +22,8 @@ extern __xdata uint8_t igmpEnabled;
#include "uip.h"
#include "uip_arp.h"
#pragma codeseg BANK1
#pragma constseg BANK1
#pragma codeseg BANK3
#pragma constseg BANK3
extern __xdata uint8_t cpuPort;
extern __xdata uint8_t sfr_data[4];
+607
View File
@@ -0,0 +1,607 @@
/*
* LACP (IEEE 802.3ad Clause 43) driver for the RTL837x platform.
* This code is in the Public Domain.
*/
// #define DEBUG
#pragma codeseg BANK3
#pragma constseg BANK3
#include <stdint.h>
#include "rtl837x_common.h"
#include "rtl837x_regs.h"
#include "rtl837x_lacp.h"
#include "rtl837x_trap.h"
#include "rtl837x_port.h"
#include "uip.h"
#include "machine.h"
extern __code const struct machine machine;
extern __xdata struct machine_runtime machine_detected; /* runtime chip detection, owned by rtl837x_port.c */
extern __xdata uint16_t management_vlan; /* owned by rtlplayground.c; suppressed per-frame for slow protocols */
extern __xdata struct uip_eth_addr uip_ethaddr;
extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2];
/* lacpEnabled: extern in rtl837x_lacp.h, owned by rtlplayground.c */
/* 20-byte Actor/Partner information body (802.3ad 43.4.2.2), network byte order */
struct lacp_info {
uint16_t sys_prio;
uint8_t sys[6];
uint16_t key;
uint16_t port_prio;
uint16_t port;
uint8_t state;
uint8_t reserved[3];
};
/* Outgoing LACPDU (no VLAN tag on TX, as in stp_pkt) */
struct lacpdu {
uint8_t dst[6];
uint8_t src[6];
struct rtl_tag rtl_tag;
uint16_t ethertype; /* 0x8809 */
uint8_t subtype; /* 0x01 */
uint8_t version; /* 0x01 */
uint8_t tlv_actor; /* 0x01 */
uint8_t actor_len; /* 0x14 */
struct lacp_info actor;
uint8_t tlv_partner; /* 0x02 */
uint8_t partner_len; /* 0x14 */
struct lacp_info partner;
uint8_t tlv_collector; /* 0x03 */
uint8_t collector_len; /* 0x10 */
uint16_t collector_max_delay;
uint8_t collector_reserved[12];
uint8_t tlv_terminator; /* 0x00 */
uint8_t terminator_len; /* 0x00 */
uint8_t terminator_reserved[50];
};
/* Incoming LACPDU, split because a VLAN tag sits between the two only when the
* frame was forwarded: a trapped one carries none (cf. stp_pkt_in) */
struct lacpdu_in_hdr {
uint8_t dst[6];
uint8_t src[6];
struct rtl_tag rtl_tag;
};
struct lacpdu_in {
uint16_t ethertype;
uint8_t subtype;
uint8_t version;
uint8_t tlv_actor;
uint8_t actor_len;
struct lacp_info actor;
uint8_t tlv_partner;
uint8_t partner_len;
struct lacp_info partner;
};
__xdata uint8_t lacp_rxbody; /* offset of the LACPDU body: the RTL tag, plus a VLAN tag when forwarded */
#define LACP_O ((__xdata struct lacpdu *)&uip_buf[RTL_FRAME_DESC_SIZE])
#define LACP_H ((__xdata struct lacpdu_in_hdr *)&uip_buf[0])
#define LACP_I ((__xdata struct lacpdu_in *)&uip_buf[lacp_rxbody])
/* --- Per-port participant state (802.3ad 43.4.7). Indexed by physical port. --- */
__xdata uint8_t lacp_actor_state[10]; /* our Actor_State flags */
__xdata uint8_t lacp_rx_state[10]; /* LACP_RX_* receive machine */
__xdata uint8_t lacp_partner_state[10]; /* last Partner_State we saw */
__xdata uint8_t lacp_partner_sys[10][6]; /* partner System ID */
__xdata uint16_t lacp_partner_key[10]; /* partner Key */
__xdata uint16_t lacp_partner_port[10]; /* partner Port number */
/* The Partner block echoes the peer's actor identity verbatim, see
* doc/link_aggregation.md. */
__xdata uint16_t lacp_partner_sys_prio[10]; /* partner System priority */
__xdata uint16_t lacp_partner_port_prio[10]; /* partner Port priority */
__xdata uint16_t lacp_periodic[10]; /* down-counter to next TX */
__xdata uint16_t lacp_timeout[10]; /* down-counter to partner expiry*/
__xdata uint8_t lacp_ntt[10]; /* Need-To-Transmit flag */
/* The timers step every four system ticks (50 Hz) whatever the pace of the
* main loop; a long pass is caught up on, a few steps at a time. */
#define LACP_TICK_STEP (SYS_TICK_HZ / 50)
#define LACP_CATCH_UP 8
__xdata uint8_t lacp_tick_last;
extern volatile __xdata uint32_t ticks;
/* Per-LAG state: the hardware trunk groups run LACP independently. */
__xdata uint16_t lacp_lag_ports[LACP_NUM_LAGS];
__xdata uint8_t lacp_port_lag[10];
__xdata uint8_t lacp_agg_sys[LACP_NUM_LAGS][6];
__xdata uint8_t lacp_agg_valid[LACP_NUM_LAGS];
__xdata uint16_t lacp_members_last[LACP_NUM_LAGS];
__xdata uint16_t lacp_scratch_mask;
__xdata uint8_t lacp_scratch_flag;
__xdata uint8_t lacp_scratch_lag;
/* Per-port RX LACPDU count - primarily a hardware bring-up diagnostic to
* verify slow-protocol frames actually reach the CPU port (see "lacp show") */
__xdata uint16_t lacp_rx_count[10];
void lacp_mux_update(void) __banked; /* defined below, used from lacp_in */
static uint8_t lacp_sys_eq(__xdata uint8_t *a, __xdata uint8_t *b)
{
for (uint8_t i = 0; i < 6; i++) {
if (a[i] != b[i])
return 0;
}
return 1;
}
/*
* Selection logic (simplified 43.4.14): a port may join the aggregator when
* we have fresh partner info, the partner considers the link aggregatable,
* and the partner is the same system the aggregator was elected for.
*/
static uint8_t lacp_port_selected(uint8_t port)
{
uint8_t lag = lacp_port_lag[port];
if (lag == LACP_LAG_NONE) /* port not in any LACP LAG */
return 0;
if (lacp_rx_state[port] != LACP_RX_CURRENT)
return 0;
if (!(lacp_partner_state[port] & LACP_STATE_AGGREGATION))
return 0;
if (!lacp_agg_valid[lag])
return 0;
return lacp_sys_eq(lacp_partner_sys[port], lacp_agg_sys[lag]);
}
/*
* Mux machine (simplified coupled control, 43.4.15): SYNC follows selection;
* COLLECTING+DISTRIBUTING additionally require the partner to be in sync.
* Returns 1 if the actor state changed (caller sets NTT).
*/
static uint8_t lacp_mux_machine(uint8_t port)
{
uint8_t old = lacp_actor_state[port];
uint8_t new = old;
if (lacp_port_selected(port)) {
new |= LACP_STATE_SYNC;
if (lacp_partner_state[port] & LACP_STATE_SYNC)
new |= LACP_STATE_COLLECTING | LACP_STATE_DISTRIBUTING;
else
new &= ~(LACP_STATE_COLLECTING | LACP_STATE_DISTRIBUTING);
} else {
new &= ~(LACP_STATE_SYNC | LACP_STATE_COLLECTING | LACP_STATE_DISTRIBUTING);
}
if (new == old)
return 0;
lacp_actor_state[port] = new;
return 1;
}
#define port_bit(p) (((uint8_t)1) << (p))
/* Build and emit one LACPDU out physical port `port` (802.3ad 43.4.2). */
void lacp_send(uint8_t port) __banked
{
memset((__xdata uint8_t *)LACP_O, 0, sizeof(struct lacpdu));
LACP_O->dst[0] = 0x01; LACP_O->dst[1] = 0x80; LACP_O->dst[2] = 0xc2;
LACP_O->dst[3] = 0x00; LACP_O->dst[4] = 0x00; LACP_O->dst[5] = LACP_DST5;
memcpy(LACP_O->src, uip_ethaddr.addr, 6);
LACP_O->rtl_tag.tag = HTONS(RTL_FRAME_TAG_ID);
LACP_O->rtl_tag.version = RTL_FRAME_TAG_VERSION;
LACP_O->rtl_tag.reason = 0x00;
/* Must go through HTONS like every other tag field: writing 0x0020 raw put
* the bits in the wrong byte (on the wire 0x2000 = EFID, not LEARN_DIS), so
* the ASIC could not parse the tag and forwarded the frame with the 0x8899
* header still attached - the partner then saw ethertype 0x8899 instead of
* 0x8809 and ignored the LACPDU. KEEP additionally stops the switch from
* rewriting the 802.1Q tag format of a CPU-injected frame (as mainline). */
LACP_O->rtl_tag.flags = HTONS(RTL_TAG_LEARN_DIS | RTL_TAG_KEEP);
/* ALLOW cleared => this word is the *forwarding* port mask, i.e. directed
* egress to exactly this port (what mainline does). Setting ALLOW instead
* makes it an allowance/permission mask on top of a normal lookup, which
* for a one-hot mask resolves to an empty egress set and the frame is
* silently dropped - verified on hardware. */
LACP_O->rtl_tag.pmask = HTONS(port_bit(port)); /* egress this port only */
LACP_O->ethertype = HTONS(SLOW_PROTO_ETHERTYPE);
LACP_O->subtype = SLOW_PROTO_SUBTYPE_LACP;
LACP_O->version = LACP_VERSION;
/* Actor TLV */
LACP_O->tlv_actor = LACP_TLV_ACTOR;
LACP_O->actor_len = LACP_TLV_LEN_INFO;
LACP_O->actor.sys_prio = HTONS(LACP_SYS_PRIO);
memcpy(LACP_O->actor.sys, uip_ethaddr.addr, 6);
/* Per-LAG Actor Key so a partner never merges ports of our different LAGs
* into one aggregate (only ever called for ports in a LACP LAG). */
LACP_O->actor.key = HTONS((uint16_t)(lacp_port_lag[port] + 1));
LACP_O->actor.port_prio = HTONS(LACP_DEF_PORT_PRIO);
LACP_O->actor.port = HTONS((uint16_t)port + 1); /* 1-based port id */
LACP_O->actor.state = lacp_actor_state[port];
/* Partner TLV: echo the last partner info we recorded, VERBATIM. A Linux
* 802.3ad partner (__record_pdu) only accepts the SYNC bit we set above if
* this block mirrors its own actor identity exactly - system+priority, key,
* port+priority. Priorities must be echoed, not hardcoded: Linux defaults to
* system priority 0xffff, so a hardcoded 0 here cleared its partner-SYNC
* (0x3f->0x37) and the aggregate never collected/distributed. */
LACP_O->tlv_partner = LACP_TLV_PARTNER;
LACP_O->partner_len = LACP_TLV_LEN_INFO;
LACP_O->partner.sys_prio = HTONS(lacp_partner_sys_prio[port]);
memcpy(LACP_O->partner.sys, lacp_partner_sys[port], 6);
LACP_O->partner.key = HTONS(lacp_partner_key[port]);
LACP_O->partner.port_prio = HTONS(lacp_partner_port_prio[port]);
LACP_O->partner.port = HTONS(lacp_partner_port[port]);
LACP_O->partner.state = lacp_partner_state[port];
/* Collector TLV + Terminator */
LACP_O->tlv_collector = LACP_TLV_COLLECTOR;
LACP_O->collector_len = LACP_TLV_LEN_COLLECTOR;
LACP_O->collector_max_delay = 0;
LACP_O->tlv_terminator = LACP_TLV_TERMINATOR;
LACP_O->terminator_len = 0x00;
lacp_ntt[port] = 0;
/* Slow-protocol frames are link-local and must egress untagged. With a
* management VLAN set, tcpip_output() splices an 802.1Q tag after the SA,
* shifting our in-frame rtl_tag out of the position the ASIC expects - so it
* fails to parse/strip the CPU tag and the 0x8899 header leaks onto the wire
* (the partner then ignores the LACPDU). Suppress the VLAN insert for this
* frame only; management traffic keeps its tag. Verified on HW: with the
* insert suppressed the LACPDU egresses as clean 0x8809 and the bond
* partner converges (partner MAC becomes the switch, both links aggregate). */
uint16_t saved_mgmt_vlan = management_vlan;
management_vlan = 0;
uip_len = sizeof(struct lacpdu);
tcpip_output();
management_vlan = saved_mgmt_vlan;
}
/*
* Process an incoming LACPDU. Called from the rtlplayground.c dispatch when a
* frame with DA 01:80:C2:00:00:02 and ethertype 0x8809/subtype 0x01 arrives.
* The ingress port is the low nibble of rtl_tag.pmask on RX.
*/
void lacp_in(void) __banked
{
lacp_rxbody = TRAP_RX_BODY();
if (uip_len < lacp_rxbody + sizeof(struct lacpdu_in)) {
uip_len = 0;
return;
}
uip_len = 0;
if (LACP_I->ethertype != HTONS(SLOW_PROTO_ETHERTYPE)
|| LACP_I->subtype != SLOW_PROTO_SUBTYPE_LACP)
return;
/* Per rtl837x_common.h, pmask carries a 4-bit port number on RX
* (hardware-verified on RTL8373: per-port rx counters track the actual
* ingress port through full LACP convergence). */
uint8_t port = ((uint8_t)HTONS(LACP_H->rtl_tag.pmask)) & 0x0f;
if (port < machine.min_port || port > machine.max_port)
return;
/* Only ports assigned to a LACP-mode LAG participate in the protocol. */
uint8_t lag = lacp_port_lag[port];
if (lag == LACP_LAG_NONE)
return;
#ifdef DEBUG
print_string("LACP in, port "); print_byte(port);
print_string(" partner_state "); print_byte(LACP_I->actor.state); write_char('\n');
#endif
lacp_rx_count[port]++;
/* Record partner = the remote's Actor block (802.3ad 43.4.9 recordPDU) */
memcpy(lacp_partner_sys[port], LACP_I->actor.sys, 6);
lacp_partner_key[port] = HTONS(LACP_I->actor.key);
lacp_partner_port[port] = HTONS(LACP_I->actor.port);
lacp_partner_sys_prio[port] = HTONS(LACP_I->actor.sys_prio);
lacp_partner_port_prio[port] = HTONS(LACP_I->actor.port_prio);
lacp_partner_state[port] = LACP_I->actor.state;
/* Receive machine -> CURRENT, (re)arm partner timeout (43.4.12) */
lacp_rx_state[port] = LACP_RX_CURRENT;
lacp_timeout[port] = (LACP_I->actor.state & LACP_STATE_TIMEOUT)
? LACP_SHORT_TIMEOUT : LACP_LONG_TIMEOUT;
/* Elect this LAG's aggregator partner system on first contact (43.4.14) */
if (!lacp_agg_valid[lag]) {
memcpy(lacp_agg_sys[lag], LACP_I->actor.sys, 6);
lacp_agg_valid[lag] = 1;
}
/* Selection + Mux for this port; state change => Need-To-Transmit */
if (lacp_mux_machine(port))
lacp_ntt[port] = 1;
/* update_NTT (43.4.12): if the partner's view of us is stale (their
* Partner block does not match our actor state/port), tell them again. */
if (LACP_I->partner.state != lacp_actor_state[port]
|| HTONS(LACP_I->partner.port) != (uint16_t)port + 1)
lacp_ntt[port] = 1;
lacp_mux_update();
}
/* Recompute each LACP LAG's hardware trunk membership: a port joins its LAG's
* trunk once it is fully in sync (SYNC+COLLECTING+DISTRIBUTING and the partner
* in SYNC). Each LACP-mode LAG is programmed independently (simplified 43.4.15). */
void lacp_mux_update(void) __banked
{
for (uint8_t lag = 0; lag < LACP_NUM_LAGS; lag++) {
if (!lacp_lag_ports[lag]) /* LAG not under LACP management */
continue;
lacp_scratch_mask = 0;
for (uint8_t i = machine.min_port; i <= machine.max_port; i++) {
if (lacp_port_lag[i] != lag)
continue;
if ((lacp_actor_state[i] & LACP_STATE_FULL) == LACP_STATE_FULL
&& (lacp_partner_state[i] & LACP_STATE_SYNC))
lacp_scratch_mask |= port_bit(i);
}
/* Program the trunk only when this LAG's membership actually changed
* - this runs on every timer tick. */
if (lacp_scratch_mask != lacp_members_last[lag]) {
lacp_members_last[lag] = lacp_scratch_mask;
port_lag_members_set(lag, lacp_scratch_mask);
}
}
}
static void lacp_step(void)
{
for (uint8_t i = machine.min_port; i <= machine.max_port; i++) {
if (lacp_port_lag[i] == LACP_LAG_NONE) /* not in a LACP LAG */
continue;
/* Periodic transmit machine (802.3ad 43.4.13) */
if (lacp_periodic[i]) {
lacp_periodic[i]--;
} else {
lacp_periodic[i] = (lacp_partner_state[i] & LACP_STATE_TIMEOUT)
? LACP_FAST_PERIODIC : LACP_SLOW_PERIODIC;
lacp_ntt[i] = 1;
}
/* Partner timeout -> DEFAULTED (802.3ad 43.4.12): the mux machine
* then drops SYNC/COLLECTING/DISTRIBUTING and the port leaves the
* trunk. TODO(43.4.12): churn detection is diagnostics-only, skipped. */
if (lacp_timeout[i]) {
if (!--lacp_timeout[i]) {
lacp_rx_state[i] = LACP_RX_DEFAULTED;
lacp_partner_state[i] = LACP_STATE_DEFAULTED;
if (lacp_mux_machine(i))
lacp_ntt[i] = 1;
}
}
if (lacp_ntt[i])
lacp_send(i);
}
/* Release each LAG's aggregator identity once none of its ports still
* hears the elected partner, so a re-cabled setup can elect a new one
* (43.4.14). A port that is CURRENT with some other system must not keep
* the old identity alive: it would never be selected, and neither would
* the rest of the LAG once it moves to that system too. */
for (uint8_t lag = 0; lag < LACP_NUM_LAGS; lag++) {
if (!lacp_agg_valid[lag])
continue;
uint8_t any_current = 0;
for (uint8_t i = machine.min_port; i <= machine.max_port; i++) {
if (lacp_port_lag[i] == lag && lacp_rx_state[i] == LACP_RX_CURRENT
&& lacp_sys_eq(lacp_partner_sys[i], lacp_agg_sys[lag])) {
any_current = 1;
break;
}
}
if (!any_current)
lacp_agg_valid[lag] = 0;
}
lacp_mux_update();
}
void lacp_timers(void) __banked
{
uint8_t n = LACP_CATCH_UP;
while (n-- && TICKS_DUE(lacp_tick_last, LACP_TICK_STEP)) {
lacp_tick_last += LACP_TICK_STEP;
lacp_step();
}
}
/* Bring one port up as an active-fast LACP participant (43.4.12 init). */
static void lacp_port_init(uint8_t port)
{
lacp_actor_state[port] = LACP_STATE_ACTIVITY | LACP_STATE_AGGREGATION
| LACP_STATE_TIMEOUT; /* active + fast */
lacp_rx_state[port] = LACP_RX_INITIALIZE;
lacp_partner_state[port] = LACP_STATE_DEFAULTED;
memset(lacp_partner_sys[port], 0, 6);
lacp_partner_key[port] = 0;
lacp_partner_port[port] = 0;
lacp_periodic[port] = LACP_FAST_PERIODIC;
lacp_timeout[port] = 0;
lacp_ntt[port] = 1; /* announce ourselves immediately */
lacp_rx_count[port] = 0;
}
/* Return a port to non-LACP state. (Port isolation is no longer touched - a
* trapped LACPDU never enters the forwarding path, so a sibling cannot see
* it, which is what the isolation workaround was for.) */
static void lacp_port_release(uint8_t port)
{
lacp_actor_state[port] = 0;
lacp_ntt[port] = 0;
lacp_partner_state[port] = 0;
lacp_rx_state[port] = LACP_RX_INITIALIZE;
lacp_rx_count[port] = 0;
memset(lacp_partner_sys[port], 0, 6);
}
/* True while any LAG is under LACP management (drives the global RMA trap). */
static uint8_t lacp_any_lag(void)
{
for (uint8_t l = 0; l < LACP_NUM_LAGS; l++)
if (lacp_lag_ports[l])
return 1;
return 0;
}
/*
* Turn the LACP engine on: trap the Slow-Protocols group to the CPU port. The
* trap acts on the address before the VLAN lookup and before the ingress
* filters, so delivery needs no L2 entry and no PVID of its own.
*/
static void lacp_engine_on(void)
{
lacp_tick_last = (uint8_t)ticks;
lacpEnabled = 1;
trap_rma_set(RTL837X_RMA2_CONF, RTL837X_RMA_ACT_TRAP);
}
/* Turn the LACP engine off: stop delivering slow-protocols to the CPU. */
static void lacp_engine_off(void)
{
trap_rma_set(RTL837X_RMA2_CONF, RTL837X_RMA_ACT_DROP);
lacpEnabled = 0;
}
/* One-time boot init: no LAG runs LACP yet, so every port maps to LACP_LAG_NONE.
* MUST run before any "lag ... lacp" config replay - xdata is not zeroed to the
* 0xff sentinel, and a stray 0 would make a port look like it belongs to LAG 0. */
void lacp_init(void) __banked
{
for (uint8_t i = 0; i < 10; i++) {
lacp_port_lag[i] = LACP_LAG_NONE;
lacp_actor_state[i] = 0;
lacp_ntt[i] = 0;
}
for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) {
lacp_lag_ports[l] = 0;
lacp_agg_valid[l] = 0;
lacp_members_last[l] = 0;
}
lacpEnabled = 0;
}
/*
* Assign a candidate-port mask to LACP-mode LAG `lag` (`lag <n> lacp <ports>`).
* ports == 0 removes the LAG from LACP management. Ports leaving the LAG are
* returned to normal switching; ports joining start the protocol immediately.
* The engine's RMA trap is enabled on the first LACP LAG and torn down with the
* last. Never wipes a static "lag" the user configured on a different group.
*/
void lacp_lag_set(uint8_t lag, uint16_t ports) __banked
{
if (lag >= LACP_NUM_LAGS)
return;
if (ports && ports == lacp_lag_ports[lag])
return;
lacp_scratch_flag = lacp_any_lag();
/* Drop ports that were in this LAG but are not in the new mask. */
for (uint8_t i = machine.min_port; i <= machine.max_port; i++) {
if (lacp_port_lag[i] == lag && !(ports & port_bit(i))) {
lacp_port_lag[i] = LACP_LAG_NONE;
lacp_port_release(i);
}
}
/* Release this LAG's trunk: LACP's own, or the static one a group put
* under LACP still has. Taking the group out (ports == 0) leaves a static
* trunk alone. */
if (lacp_members_last[lag] || ports) {
lacp_members_last[lag] = 0;
port_lag_members_set(lag, 0);
}
lacp_agg_valid[lag] = 0;
lacp_lag_ports[lag] = ports;
if (ports && !lacp_scratch_flag) /* first LACP LAG: bring the engine up first */
lacp_engine_on();
/* Adopt the new member ports (with the engine already up). A port that
* belonged to another LACP LAG leaves that LAG's candidate set first. */
for (uint8_t i = machine.min_port; i <= machine.max_port; i++) {
if (!(ports & port_bit(i)))
continue;
lacp_scratch_lag = lacp_port_lag[i];
if (lacp_scratch_lag != LACP_LAG_NONE && lacp_scratch_lag != lag) {
lacp_lag_ports[lacp_scratch_lag] &= ~port_bit(i);
if (!lacp_lag_ports[lacp_scratch_lag]) {
lacp_agg_valid[lacp_scratch_lag] = 0;
if (lacp_members_last[lacp_scratch_lag]) {
lacp_members_last[lacp_scratch_lag] = 0;
port_lag_members_set(lacp_scratch_lag, 0);
}
}
}
lacp_port_lag[i] = lag;
lacp_port_init(i);
}
if (!lacp_any_lag() && lacp_scratch_flag) /* removed the last LACP LAG */
lacp_engine_off();
lacp_mux_update(); /* a LAG that lost a port to this one drops it from its trunk */
}
/* "lacp off": remove every LACP LAG (releases their trunks, stops the engine).
* There is no "lacp on": the engine comes up with the first "lag <n> lacp". */
void lacp_off(void) __banked
{
for (uint8_t l = 0; l < LACP_NUM_LAGS; l++)
if (lacp_lag_ports[l])
lacp_lag_set(l, 0);
}
/* "lacp show": per-port protocol state - the primary bring-up diagnostic.
* A stuck rx=0 counter means slow-protocol frames never reach the CPU port
* (see the RMA note in lacp_engine_on()). */
void lacp_show(void) __banked
{
print_string("LACP "); print_string(lacpEnabled ? "on" : "off"); write_char('\n');
for (uint8_t l = 0; l < LACP_NUM_LAGS; l++) {
if (!lacp_lag_ports[l])
continue;
print_string("lag "); print_byte(l + 1);
print_string(" ports "); print_short(lacp_lag_ports[l]);
print_string(" aggregator ");
if (lacp_agg_valid[l]) {
for (uint8_t j = 0; j < 6; j++)
print_byte(lacp_agg_sys[l][j]);
} else {
print_string("(none)");
}
print_string(" members "); print_short(lacp_members_last[l]); write_char('\n');
}
for (uint8_t i = machine.min_port; i <= machine.max_port; i++) {
print_string("port "); print_byte(machine.log_to_phys_port[i]);
print_string(" actor "); print_byte(lacp_actor_state[i]);
print_string(" partner "); print_byte(lacp_partner_state[i]);
print_string(" rxst "); print_byte(lacp_rx_state[i]);
print_string(" rx "); print_short(lacp_rx_count[i]);
print_string(" psys ");
for (uint8_t j = 0; j < 6; j++)
print_byte(lacp_partner_sys[i][j]);
write_char('\n');
}
}
+101
View File
@@ -0,0 +1,101 @@
#ifndef _RTL837X_LACP_H_
#define _RTL837X_LACP_H_
/*
* LACP (IEEE 802.3ad Clause 43) for the RTL837x platform.
* This code is in the Public Domain.
*/
#include <stdint.h>
void lacp_init(void) __banked; /* boot init: clear per-LAG state */
void lacp_in(void) __banked;
void lacp_timers(void) __banked;
void lacp_off(void) __banked;
void lacp_show(void) __banked; /* "lacp show" - per-port state + RX counters */
/* Assign a candidate-port mask to a LACP-mode LAG (`lag <n> lacp <ports>`).
* ports == 0 removes the LAG from LACP management. Enables the engine on first
* LACP LAG and tears it down when the last one goes away. */
void lacp_lag_set(uint8_t lag, uint16_t ports) __banked;
/*
* Per-LAG LACP: each of the 4 hardware trunk groups (0-3) can independently run
* LACP on an admin-assigned set of candidate ports, forming its own aggregator
* (its own elected partner System). A port participates in LACP only if it has
* been assigned to a LACP-mode LAG (lacp_port_lag[port] != LACP_LAG_NONE).
* Ports of the same LAG that converge with a consistent partner join that LAG's
* hardware trunk. This is the port-channel model: `lag <n> lacp <ports>`.
*/
#define LACP_NUM_LAGS 4
#define LACP_LAG_NONE 0xff /* port not assigned to any LACP LAG */
/* Protocol state, exposed read-only for the web UI (page_impl.c send_lacp())
* and the serial console. Owned by rtl837x_lacp.c / rtlplayground.c. */
extern __xdata uint8_t lacpEnabled;
extern __xdata uint8_t lacp_actor_state[10];
extern __xdata uint8_t lacp_partner_state[10];
extern __xdata uint8_t lacp_rx_state[10];
extern __xdata uint8_t lacp_partner_sys[10][6];
extern __xdata uint16_t lacp_rx_count[10];
extern __xdata uint8_t lacp_port_lag[10]; /* LAG (0-3) a port runs LACP on, or LACP_LAG_NONE */
extern __xdata uint16_t lacp_lag_ports[LACP_NUM_LAGS]; /* admin candidate-port mask per LACP LAG (0 = LAG not LACP) */
extern __xdata uint8_t lacp_agg_sys[LACP_NUM_LAGS][6]; /* elected partner System per LAG */
extern __xdata uint8_t lacp_agg_valid[LACP_NUM_LAGS]; /* aggregator elected for this LAG */
extern __xdata uint16_t lacp_members_last[LACP_NUM_LAGS];/* trunk members we last programmed per LAG */
/* Slow-Protocols / LACPDU identifiers (802.3ad 43.4) */
#define SLOW_PROTO_ETHERTYPE 0x8809
#define SLOW_PROTO_SUBTYPE_LACP 0x01
#define LACP_VERSION 0x01
#define LACP_DST5 0x02 /* 01:80:C2:00:00:02 last octet */
/* LACPDU TLV framing (802.3ad 43.4.2): type bytes + fixed TLV lengths. */
#define LACP_TLV_ACTOR 0x01
#define LACP_TLV_PARTNER 0x02
#define LACP_TLV_COLLECTOR 0x03
#define LACP_TLV_TERMINATOR 0x00
#define LACP_TLV_LEN_INFO 0x14 /* Actor/Partner Information TLV length */
#define LACP_TLV_LEN_COLLECTOR 0x10 /* Collector TLV length */
/* Our aggregation identity: System priority is a constant (Linux 802.3ad
* defaults to the same 0xffff); the default Actor port priority likewise. */
#define LACP_SYS_PRIO 0xffff
#define LACP_DEF_PORT_PRIO 0x00ff
/* Actor_State / Partner_State flag bits (802.3ad 43.4.2) */
#define LACP_STATE_ACTIVITY 0x01 /* 1 = Active LACP (send periodically) */
#define LACP_STATE_TIMEOUT 0x02 /* 1 = Short (fast) timeout */
#define LACP_STATE_AGGREGATION 0x04 /* 1 = link is aggregatable */
#define LACP_STATE_SYNC 0x08 /* 1 = in sync with partner */
#define LACP_STATE_COLLECTING 0x10
#define LACP_STATE_DISTRIBUTING 0x20
#define LACP_STATE_DEFAULTED 0x40 /* partner info is administrative default*/
#define LACP_STATE_EXPIRED 0x80 /* wire-format doc; unused by the simplified RX machine */
/* Fully-participating actor: in sync AND collecting AND distributing. */
#define LACP_STATE_FULL (LACP_STATE_SYNC | LACP_STATE_COLLECTING | LACP_STATE_DISTRIBUTING)
/*
* Timer units: one step of the LACP timers, taken every four system ticks,
* 50 per second. A long pass of the main loop is caught up on a few steps at
* a time, so a period is a range and the worst case is what matters.
*
* The transmit periods are not free to choose. Our own timeouts only decide
* how patient we are, but our TX period decides when the PARTNER gives up on
* us: 802.3ad pairs fast periodic 1 s with a 3 s short timeout, and slow
* periodic 30 s with a 90 s long timeout. A period whose worst case reaches
* the partner's threshold makes it expire us intermittently, which reads as
* an aggregate that works but never stops churning. */
#define LACP_FAST_PERIODIC 0x0032 /* fast TX 1 s; partner expires at 3 s */
#define LACP_SLOW_PERIODIC 0x05dc /* slow TX 30 s; partner expires at 90 s */
#define LACP_SHORT_TIMEOUT 0x00c8 /* silent partner dropped after 4 s (802.3ad: 3 s) */
#define LACP_LONG_TIMEOUT 0x1770 /* long-timeout variant, after 120 s (802.3ad: 90 s) */
/* Per-port LACP receive-machine state (802.3ad 43.4.12) */
#define LACP_RX_INITIALIZE 0
#define LACP_RX_PORT_DISABLED 1 /* numbering reserved (exported via /lacp.json "rs") */
#define LACP_RX_EXPIRED 2 /* numbering reserved (exported via /lacp.json "rs") */
#define LACP_RX_DEFAULTED 3
#define LACP_RX_CURRENT 4
#endif
+4
View File
@@ -284,6 +284,10 @@
* Loop detection / STP
*/
#define RTL8373_RLDP_TIMER 0x1074
/* Per reserved-multicast-address action. The registers are packed and
* consecutive by ENTRY rather than by the byte of the address: entry 0 is
* 01:80:C2:00:00:00 at 0x4ecc and entry 2 is ...:02 (Slow Protocols / LACP)
* at 0x4ed4, but entry 5 is ...:08, so the step of 4 counts entries. */
#define RTL837X_RMA0_CONF 0x4ecc
#define RTL837X_RMA2_CONF 0x4ed4
#define RTL837X_RMA_ACT_MASK 0x30
+19 -6
View File
@@ -13,6 +13,7 @@
#include "rtl837x_port.h"
#include "rtl837x_trap.h"
#include "rtl837x_stp.h"
#include "rtl837x_lacp.h"
#include "rtl837x_igmp.h"
#include "rtl837x_leds.h"
#include "rtl837x_bandwidth.h"
@@ -133,6 +134,7 @@ __xdata bool stp_enabled;
__xdata uint8_t igmpEnabled;
__xdata bool lldp_enabled;
__xdata char hostname[24]; /* device hostname, default set at boot, see rtl837x_common.h */
__xdata uint8_t lacpEnabled;
__code const uint16_t bit_mask[16] = {
0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0080,
@@ -1059,12 +1061,18 @@ void handle_rx(void)
print_byte(uip_buf[3]); print_byte(uip_buf[4]); print_byte(uip_buf[5]); write_char('\n');
print_string(" MGMT-VLAN: "); print_short(management_vlan); write_char('\n');
#endif
if (stp_enabled && uip_buf[0] == 0x01 && uip_buf[1] == 0x80 && uip_buf[2] == 0xc2 // STP packet?
&& uip_buf[3] == 0x00 && uip_buf[4] == 0x00 && uip_buf[5] == 0x00) {
stp_in();
if (uip_len) {
print_string("STP TX\n");
tcpip_output();
if ((stp_enabled || lacpEnabled) && uip_buf[0] == 0x01 && uip_buf[1] == 0x80
&& uip_buf[2] == 0xc2 && uip_buf[3] == 0x00 && uip_buf[4] == 0x00) { // 01:80:C2:00:00:0x
if (stp_enabled && uip_buf[5] == 0x00) { // STP/RSTP BPDU
stp_in();
if (uip_len) {
print_string("STP TX\n");
tcpip_output();
}
} else if (lacpEnabled && uip_buf[5] == 0x02) { // LACPDU (slow protocols)
lacp_in();
if (uip_len)
tcpip_output();
}
} else if (igmpEnabled && uip_buf[0] == 0x01 && uip_buf[1] == 0x00 && uip_buf[2] == 0x5e // IPv4-MC packet?
&& igmp_packet_handler()) {
@@ -1290,6 +1298,9 @@ static void handle_tick(void)
stp_timers();
}
}
// If LACP enabled, drive its machines (own tick divider lives in the banked module)
if (lacpEnabled)
lacp_timers();
health_phase(HEALTH_PH_STP);
lldp_tick();
health_phase(HEALTH_LLDP);
@@ -1792,6 +1803,8 @@ void main(void)
#endif
stp_enabled = 0;
stp_defaults(); /* 802.1D/w default config before any "stp ..." replay */
lacpEnabled = 0;
lacp_init(); /* clear per-LAG state (port->LAG map = NONE) before any config replay */
nic_setup();
REG_SET(RTL837X_NIC_INT_MSK, NIC_INT_RXIE);
trap_init();
+9
View File
@@ -59,15 +59,24 @@ $(BUILDDIR)/test_page_json: test_page_json.c $(TABLE_ENV) hw_mock.h | $(BUILDDIR
$(BUILDDIR)/test_httpd_tx: test_httpd_tx.c $(SRCROOT)/httpd/httpd.c $(SRCROOT)/uip/uip.c | $(BUILDDIR)
$(CC) $(CFLAGS) $(filter %.c,$^) -o $@ $(LDFLAGS)
# Sandboxes with a Makefile of their own; each fails the run when it fails.
SUBTESTS = lacp port_hash
run: $(TESTS)
@fail=0; for t in $(TESTS); do \
echo "=== $$t ==="; \
$$t || fail=1; \
echo; \
done; \
for d in $(SUBTESTS); do \
echo "=== $$d ==="; \
$(MAKE) -C $$d test || fail=1; \
echo; \
done; \
if [ $$fail -ne 0 ]; then echo "TESTS FAILED"; exit 1; else echo "ALL TESTS PASSED"; fi
clean:
rm -rf $(BUILDDIR)
@for d in $(SUBTESTS); do $(MAKE) -C $$d clean; done
.PHONY: all run clean
+2
View File
@@ -48,6 +48,8 @@ fires — so it drops straight into CI.
| `test_port_tables` | `rtl837x_port.c` | VLAN entry layout and round trip, PVID register sharing, static multicast and management entries, per-port flush, trunk membership and hash seed |
| `test_page_json` | `httpd/page_impl.c` + `rtl837x_port.c` | `/vlan.json`, `/vlanlist`, `/l2.json` (walk, wrap marker, paging inside `outbuf`), 64-bit counters in `/status.json` and `/counters.json` |
| `test_httpd_tx` | `httpd/httpd.c` + `uip/uip.c` | a GET of a static file against a client that moves its receive window: ACK accounting, continuation chunks out of flash, retransmission |
| `lacp/` | `rtl837x_lacp.c` | state machines against a simulated 802.3ad partner: convergence, mis-cabling, expiry, re-election, two groups, LACPDU containment entries |
| `port_hash/` | `port_lag_members_set()` | a LAG seeds its own hash register |
## Adding a test for another module
1. Write `test_<module>.c` with `main()` driving the module's entry points and
+16
View File
@@ -16,6 +16,7 @@
#include "syslog.h"
#include "rtl837x_phy.h"
#include "support.h"
#include "rtl837x_lacp.h"
/* ---- console: everything lands in out_buf (support.c) ---- */
void print_byte(uint8_t v)
@@ -124,3 +125,18 @@ uint8_t stp_prio = 0x80, stp_root_port = 0xff, stp_rstp = 1, stp_txhold = 6;
uint8_t sfp_buf[16];
bool sfp_read_block(uint8_t slot, uint8_t reg, uint8_t len) { (void)slot; (void)reg; (void)len; return false; }
void print_phys_port(uint8_t port) { itoa_short(machine.log_to_phys_port[port]); }
uint8_t lacpEnabled;
uint8_t lacp_actor_state[10];
uint8_t lacp_partner_state[10];
uint8_t lacp_rx_state[10];
uint8_t lacp_partner_sys[10][6];
uint16_t lacp_rx_count[10];
uint8_t lacp_port_lag[10] = {
LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE,
LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE, LACP_LAG_NONE,
};
uint16_t lacp_lag_ports[LACP_NUM_LAGS];
uint8_t lacp_agg_sys[LACP_NUM_LAGS][6];
uint8_t lacp_agg_valid[LACP_NUM_LAGS];
uint16_t lacp_members_last[LACP_NUM_LAGS];
+1
View File
@@ -0,0 +1 @@
build/
+31
View File
@@ -0,0 +1,31 @@
# Host-side sandbox for the firmware LACP state machines.
#
# Compiles the UNMODIFIED ../../rtl837x_lacp.c against shim headers: SDCC
# storage-class keywords are blanked via -D, and the module copy lives in
# build/ so quote-includes resolve to shim/ (its own directory has no real
# platform headers) instead of the firmware tree. rtl837x_trap.h is copied
# alongside it for the same reason: TRAP_RX_BODY() is what delivery by trap
# rests on, so the test must use the real macro, not a shim restating it.
CC ?= cc
CFLAGS = -std=c11 -Wall -Wextra -Wno-unknown-pragmas \
-D__xdata= -D__code= -D__banked= -D__reentrant= \
-Ishim -I../..
all: test
test: build/lacp_sim
./build/lacp_sim
verbose: build/lacp_sim
./build/lacp_sim -v
build/lacp_sim: lacp_sim.c ../../rtl837x_lacp.c ../../rtl837x_lacp.h ../../rtl837x_trap.h shim/*.h
mkdir -p build
cp ../../rtl837x_lacp.c build/lacp_under_test.c
cp ../../rtl837x_trap.h build/rtl837x_trap.h
$(CC) $(CFLAGS) -o $@ lacp_sim.c build/lacp_under_test.c
clean:
rm -rf build
.PHONY: all test verbose clean
+464
View File
@@ -0,0 +1,464 @@
/*
* Host-side sandbox for the RTL837x LACP implementation.
*
* Compiles the UNMODIFIED rtl837x_lacp.c against shim headers (see shim/) and
* drives it with synthetic LACPDUs from a simulated partner switch/bond. This
* exercises the real state machines that ship in the firmware image - only
* the platform below them (frame I/O, trunk registers, console) is mocked.
*
* Run: make -C test/lacp (exit code 0 = all scenarios pass)
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include "rtl837x_common.h"
#include "machine.h"
#include "uip.h"
#include "rtl837x_regs.h"
#include "rtl837x_lacp.h"
/* ---------- platform mocks ---------- */
uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2];
uint16_t uip_len;
struct uip_eth_addr uip_ethaddr = { .addr = {0x02,0x11,0x22,0x33,0x44,0x55} };
const struct machine machine = { .min_port = 0, .max_port = 7,
.log_to_phys_port = {5, 1, 2, 3, 4, 6, 7, 8, 9} };
struct machine_runtime machine_detected = { .isRTL8373 = 1 };
uint8_t lacpEnabled;
uint16_t management_vlan; /* mocked; slow-protocol path zeroes it per-frame */
static int verbose = 0;
void print_string(char *s) { if (verbose) fputs(s, stdout); }
void print_byte(uint8_t b) { if (verbose) printf("%02x", b); }
void print_short(uint16_t v) { if (verbose) printf("%04x", v); }
void write_char(char c) { if (verbose) putchar(c); }
/* Trunk programming mock: record last mask per LAG + call count.
* hw_members mirrors LAG 0 so the single-aggregator scenarios read naturally. */
static uint16_t hw_members_lag[4];
static int hw_set_calls;
#define hw_members hw_members_lag[0]
void port_lag_members_set(uint8_t lag, uint16_t members)
{
if (lag < 4)
hw_members_lag[lag] = members;
hw_set_calls++;
}
/* Reserved-multicast action mock: delivery of a LACPDU to the CPU rests on
* the action of the slow-protocols entry alone, so record what the engine
* sets. The real trap_rma_set() touches only the action field. */
static uint16_t hw_rma_reg;
static uint8_t hw_rma_act;
static int hw_rma_calls;
void trap_rma_set(uint16_t reg, uint8_t act);
void trap_rma_set(uint16_t reg, uint8_t act)
{
hw_rma_reg = reg;
hw_rma_act = act;
hw_rma_calls++;
}
/* ---------- on-wire mirrors (independent re-statement of the layout) ---------- */
struct wire_info {
uint16_t sys_prio;
uint8_t sys[6];
uint16_t key;
uint16_t port_prio;
uint16_t port;
uint8_t state;
uint8_t reserved[3];
};
struct wire_pdu_out { /* what lacp_send() writes at uip_buf+12 */
uint8_t dst[6];
uint8_t src[6];
struct rtl_tag rtl_tag;
uint16_t ethertype;
uint8_t subtype;
uint8_t version;
uint8_t tlv_actor, actor_len;
struct wire_info actor;
uint8_t tlv_partner, partner_len;
struct wire_info partner;
uint8_t tlv_collector, collector_len;
uint16_t collector_max_delay;
uint8_t collector_reserved[12];
uint8_t tlv_terminator, terminator_len;
uint8_t terminator_reserved[50];
};
struct wire_pdu_in { /* forwarded form: the hardware inserts a VLAN tag */
uint8_t dst[6];
uint8_t src[6];
struct rtl_tag rtl_tag;
struct vlan_tag vlan_tag;
uint16_t ethertype;
uint8_t subtype;
uint8_t version;
uint8_t tlv_actor, actor_len;
struct wire_info actor;
uint8_t tlv_partner, partner_len;
struct wire_info partner;
};
_Static_assert(sizeof(struct wire_info) == 18, "info TLV body must be 18");
_Static_assert(sizeof(struct wire_pdu_out) == 132, "TX pdu must be 132");
_Static_assert(sizeof(struct rtl_tag) == 8, "rtl tag must be 8");
/* ---------- TX capture ---------- */
static struct wire_pdu_out last_tx[10];
static int tx_count[10];
void tcpip_output(void)
{
struct wire_pdu_out *p = (struct wire_pdu_out *)&uip_buf[RTL_FRAME_DESC_SIZE];
uint16_t pmask = HTONS(p->rtl_tag.pmask);
for (int port = 0; port < 10; port++) {
if (pmask & (1u << port)) {
last_tx[port] = *p;
tx_count[port]++;
}
}
uip_len = 0;
}
/* ---------- simulated partner ---------- */
/* Deliver a LACPDU into the switch on `port`, from partner system `sys`.
* pstate = partner's Actor_State flags. If `echo`, the partner echoes the
* switch's last-seen actor block back (fresh view) - required for quiescence. */
static void partner_frame_full(uint8_t port, const uint8_t sys[6], uint8_t pstate,
int echo, uint16_t len, int tagged)
{
struct wire_pdu_in in;
memset(&in, 0, sizeof(in));
in.dst[0]=0x01; in.dst[1]=0x80; in.dst[2]=0xc2; in.dst[5]=0x02;
memcpy(in.src, sys, 6);
in.rtl_tag.tag = HTONS(RTL_FRAME_TAG_ID);
in.rtl_tag.pmask = HTONS(port); /* RX: 4-bit ingress port */
in.vlan_tag.svlan = HTONS(0x8100); /* the tag's TPID, which marks the body offset */
in.ethertype = HTONS(0x8809);
in.subtype = 0x01;
in.version = 0x01;
in.tlv_actor = 0x01; in.actor_len = 0x14;
in.actor.sys_prio = HTONS(0x8000);
memcpy(in.actor.sys, sys, 6);
in.actor.key = HTONS(0x0011);
in.actor.port_prio = HTONS(0x00ff);
in.actor.port = HTONS((uint16_t)port + 101);
in.actor.state = pstate;
in.tlv_partner = 0x02; in.partner_len = 0x14;
if (echo) { /* fresh view of us, from our last TX */
in.partner.sys_prio = last_tx[port].actor.sys_prio;
memcpy(in.partner.sys, last_tx[port].actor.sys, 6);
in.partner.key = last_tx[port].actor.key;
in.partner.port = last_tx[port].actor.port;
in.partner.state = last_tx[port].actor.state;
} /* else: zeros = stale view */
memcpy(uip_buf, &in, sizeof(in));
if (!tagged) { /* trapped: no VLAN tag, so the body sits earlier */
uint8_t *body = uip_buf + RTL_FRAME_DESC_SIZE + RTL_TAG_SIZE;
memmove(body, body + VLAN_TAG_SIZE,
sizeof(in) - (RTL_FRAME_DESC_SIZE + RTL_TAG_SIZE + VLAN_TAG_SIZE));
len -= VLAN_TAG_SIZE;
}
uip_len = len;
lacp_in();
}
static void partner_frame_len(uint8_t port, const uint8_t sys[6], uint8_t pstate, int echo, uint16_t len)
{
partner_frame_full(port, sys, pstate, echo, len, 1);
}
static void partner_frame(uint8_t port, const uint8_t sys[6], uint8_t pstate, int echo)
{
partner_frame_full(port, sys, pstate, echo, sizeof(struct wire_pdu_in), 1);
}
/* The same LACPDU as the reserved-multicast trap delivers it. */
static void partner_frame_trap(uint8_t port, const uint8_t sys[6], uint8_t pstate, int echo)
{
partner_frame_full(port, sys, pstate, echo, sizeof(struct wire_pdu_in), 0);
}
/* Advance time by n system ticks; the LACP timers step every fourth one */
volatile uint32_t ticks;
static void step(int n) { while (n--) { ticks++; lacp_timers(); } }
/* ---------- scenario runner ---------- */
static int failures;
#define CHECK(cond, name) do { \
if (cond) printf("PASS %s\n", name); \
else { printf("FAIL %s\n", name); failures++; } \
} while (0)
#define P_ACT LACP_STATE_ACTIVITY
#define P_AGG LACP_STATE_AGGREGATION
#define P_SYNC LACP_STATE_SYNC
#define P_COL LACP_STATE_COLLECTING
#define P_DIST LACP_STATE_DISTRIBUTING
#define P_TO LACP_STATE_TIMEOUT /* partner requests short (fast) timeout */
static const uint8_t SYS_A[6] = {0x02,0xaa,0xaa,0xaa,0xaa,0x01};
static const uint8_t SYS_B[6] = {0x02,0xbb,0xbb,0xbb,0xbb,0x02};
int main(int argc, char **argv)
{
verbose = (argc > 1 && !strcmp(argv[1], "-v"));
/* Boot init first: on host, zeroed globals would otherwise read as "every
* port belongs to LAG 0" (the firmware calls this from main() the same way) */
lacp_init();
/* T1: enable announces on every port with sane field contents */
lacp_lag_set(0, 0x00ff);
step(8);
int all_tx = 1, sane = 1;
for (int i = machine.min_port; i <= machine.max_port; i++) {
if (!tx_count[i]) all_tx = 0;
if (last_tx[i].ethertype != HTONS(0x8809) || last_tx[i].subtype != 1
|| last_tx[i].dst[5] != 0x02
|| !(last_tx[i].actor.state & (P_ACT|P_AGG|P_TO)))
sane = 0;
}
CHECK(all_tx && sane, "T1 enable: LACPDU announced on all ports, well-formed");
/* T2: partner without SYNC -> actor SYNC only, no members yet */
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO, 1);
partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO, 1);
step(8);
CHECK((lacp_actor_state[0] & P_SYNC) && !(lacp_actor_state[0] & (P_COL|P_DIST))
&& hw_members == 0,
"T2 partner not in sync: actor SYNC only, trunk empty");
/* T3: partner in sync on ports 0,1 -> full converge, members 0x0003 */
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
step(8);
CHECK(hw_members == 0x0003
&& (lacp_actor_state[0] & (P_SYNC|P_COL|P_DIST)) == (P_SYNC|P_COL|P_DIST)
&& (lacp_actor_state[1] & (P_SYNC|P_COL|P_DIST)) == (P_SYNC|P_COL|P_DIST),
"T3 convergence: both ports collecting/distributing, trunk 0x0003");
/* T3b: partner-block echo must be VERBATIM. A Linux 802.3ad partner accepts
* our SYNC only if our Partner TLV mirrors its actor identity exactly; a
* hardcoded sys_prio/port_prio (instead of the recorded value) makes Linux
* clear partner-SYNC and the bond never distributes. Verify we echo the
* partner's own priorities back, not a constant. */
CHECK(HTONS(last_tx[0].partner.sys_prio) == 0x8000
&& HTONS(last_tx[0].partner.port_prio) == 0x00ff
&& HTONS(last_tx[0].partner.key) == 0x0011
&& HTONS(last_tx[0].partner.port) == (uint16_t)0 + 101,
"T3b partner echo: recorded partner priorities/key/port echoed verbatim");
/* T4: mis-cabling - port 2 sees a DIFFERENT system: must stay out */
partner_frame(2, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
step(8);
CHECK(hw_members == 0x0003 && !(lacp_actor_state[2] & P_SYNC),
"T4 mis-cabling: port with different partner system stays out");
/* T5: register-write economy: stable state must not rewrite the trunk */
int calls_before = hw_set_calls;
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
step(64);
CHECK(hw_set_calls == calls_before,
"T5 stable state: no redundant trunk register writes");
/* T6: stale echo triggers retransmit (update_NTT) */
int txc = tx_count[0];
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 0 /* stale view of us */);
step(8);
CHECK(tx_count[0] > txc, "T6 update_NTT: stale partner view causes retransmit");
/* T7: expiry - partner silent past the short timeout: trunk drains,
* aggregator identity is released */
step(4 * LACP_SHORT_TIMEOUT + 64); /* past the short timeout */
CHECK(hw_members == 0 && lacp_agg_valid[0] == 0,
"T7 expiry: members drop to 0 and aggregator is released");
/* T8: re-convergence with a NEW partner system after release */
/* (needs a fresh announce first so the echo below carries current state) */
step(4 * 0x0100 + 8); /* let periodic TX refresh last_tx */
partner_frame(3, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
partner_frame(3, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
step(8);
CHECK(hw_members == 0x0008 && (lacp_actor_state[3] & P_COL),
"T8 re-election: new partner system forms a fresh aggregate");
/* T9: lacp off clears the trunk */
lacp_off();
CHECK(hw_members == 0 && lacpEnabled == 0, "T9 disable: trunk cleared");
/* ---- per-LAG scenarios: two independent aggregators ---- */
/* T10: lag 1 lacp {0,1} + lag 2 lacp {2,3}, both to the SAME partner
* system (worst case for sibling scoping): each converges into its own
* hardware trunk, and the engine reports enabled. */
lacp_lag_set(1, 0x0003); /* ports 0,1 */
lacp_lag_set(2, 0x000c); /* ports 2,3 */
step(8); /* announce */
for (int r = 0; r < 2; r++) { /* two rounds so echoes carry fresh state */
for (int p = 0; p < 4; p++)
partner_frame(p, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
step(8);
}
CHECK(lacpEnabled == 1
&& hw_members_lag[1] == 0x0003 && hw_members_lag[2] == 0x000c,
"T10 two LAGs: independent aggregates converge on their own trunks");
/* T11: removing one LAG releases only its trunk; the other keeps running */
lacp_lag_set(1, 0);
CHECK(hw_members_lag[1] == 0 && hw_members_lag[2] == 0x000c
&& lacpEnabled == 1,
"T11 remove one LAG: its trunk drains, the other LAG unaffected");
/* T12: a port in no LACP LAG ignores LACPDUs entirely */
int rx5_before = tx_count[5];
partner_frame(5, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
step(8);
CHECK(!(lacp_actor_state[5] & P_SYNC) && hw_members_lag[2] == 0x000c,
"T12 unassigned port: LACPDU ignored, no state change");
(void)rx5_before;
/* T13: removing the last LACP LAG shuts the engine down */
lacp_lag_set(2, 0);
CHECK(lacpEnabled == 0 && hw_members_lag[2] == 0
&& hw_rma_reg == RTL837X_RMA2_CONF && hw_rma_act == RTL837X_RMA_ACT_DROP,
"T13 last LAG removed: engine off, trunk drained, address dropped");
/* T14: while the engine runs, the slow-protocols address is trapped to
* the CPU. The trap keys on the address alone, so delivery needs no L2
* entry and nothing written per VLAN or per PVID. */
hw_rma_calls = 0;
lacp_lag_set(0, 0x0003);
CHECK(hw_rma_calls == 1 && hw_rma_reg == RTL837X_RMA2_CONF
&& hw_rma_act == RTL837X_RMA_ACT_TRAP,
"T14 engine on traps 01:80:c2:00:00:02 to the CPU");
/* T15: a trapped LACPDU arrives without the VLAN tag a forwarded one
* carries, so its body sits four bytes earlier. Both forms must parse. */
uint16_t rx_before = lacp_rx_count[0];
lacp_partner_state[0] = 0; /* all three are written past the ethertype gate */
memset(lacp_partner_sys[0], 0, 6);
partner_frame_trap(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
int trap_ok = lacp_rx_count[0] == (uint16_t)(rx_before + 1)
&& lacp_partner_state[0] == (P_ACT|P_AGG|P_TO|P_SYNC)
&& !memcmp(lacp_partner_sys[0], SYS_A, 6);
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
CHECK(trap_ok && lacp_rx_count[0] == (uint16_t)(rx_before + 2),
"T15 trapped LACPDU parses like a forwarded one");
/* T16: the partner changes one cable at a time. Port 1 starts hearing B
* while port 0 still hears A, then A goes quiet and B appears on port 0
* too. The aggregator must let go of A once no port hears it, even though
* port 1 was CURRENT the whole time, or the LAG never aggregates with B. */
lacp_lag_set(0, 0);
lacp_lag_set(0, 0x0003);
step(8);
for (int r = 0; r < 3; r++) {
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
partner_frame(1, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
step(8);
}
for (int t = 0; t < 40; t++) { /* port 1 moved to B, port 0 still on A */
partner_frame(0, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
partner_frame(1, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
step(4 * 40);
}
int stayed_out = hw_members == 0x0001 && !(lacp_actor_state[1] & P_SYNC);
for (int t = 0; t < 60; t++) /* port 0 unplugged from A, past the short timeout */
{ partner_frame(1, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1); step(4 * 40); }
for (int t = 0; t < 40; t++) { /* port 0 now on B as well */
partner_frame(0, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
partner_frame(1, SYS_B, P_ACT|P_AGG|P_TO|P_SYNC, 1);
step(4 * 40);
}
CHECK(stayed_out && hw_members == 0x0003 && !memcmp(lacp_agg_sys[0], SYS_B, 6),
"T16 re-cabling one port at a time: aggregator moves from A to B");
/* T17: a group that already has a STATIC trunk is put under LACP with the
* same ports (the LAG page's Static -> LACP switch sends only "lag N lacp").
* The static trunk must go: until the partner converges the ports are not
* an aggregate, and after a reboot the config replays only the LACP line. */
hw_members_lag[1] = 0x0030; /* static lag 2: ports 4,5 */
lacp_lag_set(1, 0x0030);
step(8);
CHECK(hw_members_lag[1] == 0 && lacp_members_last[1] == 0 && lacp_lag_ports[1] == 0x0030,
"T17 static trunk taken over: cleared while the group negotiates");
lacp_lag_set(1, 0);
/* T18: a port named in a second group moves there. The first group's
* candidate set drops it, so taking the second group out later leaves no
* orphan that group 1 still shows as a candidate but never runs LACP on. */
lacp_lag_set(1, 0x0030); /* ports 4,5 */
step(8);
for (int r = 0; r < 2; r++) {
partner_frame(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
partner_frame(5, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
step(8);
}
int converged = hw_members_lag[1] == 0x0030;
lacp_lag_set(2, 0x0060); /* ports 5,6: port 5 moves from group 2 to 3 */
int moved = lacp_lag_ports[1] == 0x0010 && lacp_port_lag[5] == 2
&& hw_members_lag[1] == 0x0010;
lacp_lag_set(2, 0);
int no_orphan = lacp_port_lag[5] == LACP_LAG_NONE && lacp_lag_ports[1] == 0x0010;
lacp_lag_set(3, 0x0010); /* the last port leaves group 2 as well */
CHECK(converged && moved && no_orphan && lacp_lag_ports[1] == 0 && lacp_agg_valid[1] == 0,
"T18 port moves between groups: old group drops it from candidates and trunk");
lacp_lag_set(3, 0);
/* T20: taking a group out of LACP leaves nothing of the partner behind
* for "lacp show" to report as if the port still heard it. */
lacp_lag_set(1, 0x0030);
step(8);
partner_frame(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
step(8);
int had_partner = lacp_rx_state[4] == LACP_RX_CURRENT && lacp_rx_count[4] > 0;
lacp_lag_set(1, 0);
CHECK(had_partner && lacp_partner_state[4] == 0 && lacp_rx_state[4] == LACP_RX_INITIALIZE
&& lacp_rx_count[4] == 0 && !memcmp(lacp_partner_sys[4], "\0\0\0\0\0\0", 6),
"T20 release: partner state, rx state, counter and system cleared");
/* T21: applying the same candidate set again (the LAG page resends it
* with every Apply) must not tear the trunk down and renegotiate. */
lacp_lag_set(1, 0x0030);
step(8);
for (int r = 0; r < 2; r++) {
partner_frame(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
partner_frame(5, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1);
step(8);
}
int before = hw_set_calls;
lacp_lag_set(1, 0x0030);
CHECK(hw_members_lag[1] == 0x0030 && hw_set_calls == before
&& (lacp_actor_state[4] & P_COL),
"T21 same ports applied again: trunk and state untouched");
lacp_lag_set(1, 0);
/* T22: a runt on the slow-protocols address must not be parsed as a
* LACPDU; the partner block would be read from stale buffer bytes. */
lacp_lag_set(1, 0x0030);
step(8);
partner_frame_len(4, SYS_A, P_ACT|P_AGG|P_TO|P_SYNC, 1, 40); /* cut before the partner block */
CHECK(lacp_rx_count[4] == 0 && lacp_rx_state[4] == LACP_RX_INITIALIZE,
"T22 truncated LACPDU ignored: the port does not record it");
lacp_lag_set(1, 0);
printf("\n%s (%d failure%s)\n", failures ? "SANDBOX: FAILURES" : "SANDBOX: ALL PASS",
failures, failures == 1 ? "" : "s");
return failures ? 1 : 0;
}
+12
View File
@@ -0,0 +1,12 @@
/* Host-build shim: rtl837x_lacp.c reads min_port/max_port and, for the
* console dump, the front-panel numbering. The real map is not the identity
* on any machine, so the harness uses a shuffled one to keep a logical index
* from passing as a physical port by accident. */
#ifndef _SHIM_MACHINE_H_
#define _SHIM_MACHINE_H_
#include <stdint.h>
struct machine { uint8_t min_port; uint8_t max_port; uint8_t log_to_phys_port[9]; };
extern const struct machine machine;
struct machine_runtime { uint8_t isRTL8373; };
extern struct machine_runtime machine_detected;
#endif
+63
View File
@@ -0,0 +1,63 @@
/*
* Host-build shim for rtl837x_common.h - just enough for rtl837x_lacp.c.
* Layouts of rtl_tag/vlan_tag and the frame-desc size mirror the real header;
* the harness static-asserts the resulting on-wire struct sizes.
*/
#ifndef _SHIM_RTL837X_COMMON_H_
#define _SHIM_RTL837X_COMMON_H_
#include <stdint.h>
#include <string.h> /* memcpy/memset - firmware provides its own */
struct rtl_tag {
uint16_t tag;
uint8_t version;
uint8_t reason;
uint16_t flags;
uint16_t pmask; /* TX: port bitmask; RX: 4-bit port number */
};
struct vlan_tag {
uint16_t svlan;
uint16_t vlan;
};
#define RTL_TAG_SIZE (sizeof (struct rtl_tag))
#define VLAN_TAG_SIZE (sizeof (struct vlan_tag))
#define RTL_FRAME_TAG_ID 0x8899
#define RTL_FRAME_TAG_VERSION 0x04
#define RTL_FRAME_DESC_SIZE 12
#define ETHER_HEADER_SIZE 14
#define HTONS(n) ((uint16_t)((((uint16_t)(n)) << 8) | (((uint16_t)(n)) >> 8)))
/* Console output - provided by the harness */
void print_string(char *s);
void print_byte(uint8_t b);
void print_short(uint16_t v);
void write_char(char c);
#endif
/* Host-build shim: port masks used by lacp_isolation_update() */
#ifndef PMASK_CPU
#define CPU_PORT 9
#define PMASK_9 0x1ff
#define PMASK_6 0x1f8
#define PMASK_CPU 0x200
#endif
/* Host-build shim: directed-egress/strip-tag bit (bit15 of TX pmask word) */
#ifndef RTL_TAG_ALLOW
#define RTL_TAG_ALLOW 0x8000
#endif
/* Host-build shim: tag flags word bits (tag_rtl8_4) */
#ifndef RTL_TAG_LEARN_DIS
#define RTL_TAG_LEARN_DIS 0x0020
#define RTL_TAG_KEEP 0x0080
#define SYS_TICK_HZ 200
extern volatile uint32_t ticks;
#define TICKS_DUE(last, period) ((uint8_t)((uint8_t)ticks - (last)) >= (period))
#endif
+7
View File
@@ -0,0 +1,7 @@
/* Host-build shim: the harness mocks trunk programming and records calls */
#ifndef _SHIM_RTL837X_PORT_H_
#define _SHIM_RTL837X_PORT_H_
#include <stdint.h>
void port_lag_members_set(uint8_t lag, uint16_t members);
void port_isolate(uint8_t port, uint16_t pmask);
#endif
+19
View File
@@ -0,0 +1,19 @@
/*
* Host-build shim for rtl837x_regs.h - the register numbers rtl837x_lacp.c
* touches, with the values from the real header so a wrong constant shows up
* as a failing expectation rather than as a silent no-op.
*/
#ifndef _SHIM_RTL837X_REGS_H_
#define _SHIM_RTL837X_REGS_H_
#include <stdint.h>
#define RTL837X_RMA2_CONF 0x4ed4
#define RTL837X_RMA_ACT_TRAP 0x10
#define RTL837X_RMA_ACT_DROP 0x20
/* Registers touched by lacp_send's CPU-port targeting */
#define RTL837X_PORT_ISOLATION_BASE 0x50c0
#define RTL837X_REG_NIC_TXCMD 0x7850
#endif
+9
View File
@@ -0,0 +1,9 @@
/* Host-build shim for uip.h - frame buffer + TX hook used by rtl837x_lacp.c */
#ifndef _SHIM_UIP_H_
#define _SHIM_UIP_H_
#include <stdint.h>
#define UIP_CONF_BUFFER_SIZE 600
struct uip_eth_addr { uint8_t addr[6]; };
extern uint16_t uip_len;
void tcpip_output(void);
#endif
+2
View File
@@ -0,0 +1,2 @@
test_lag_hash
port_lag_members_set.inc
+6
View File
@@ -0,0 +1,6 @@
test: port_lag_members_set.inc
gcc -Wall -Wextra -O1 -o test_lag_hash test_lag_hash.c && ./test_lag_hash
port_lag_members_set.inc: ../../rtl837x_port.c
awk '/^void port_lag_members_set/,/^}/' $< > $@
clean:
rm -f test_lag_hash port_lag_members_set.inc
+68
View File
@@ -0,0 +1,68 @@
/* Host test for port_lag_members_set(): the default load-balancing hash must be
* seeded into the CURRENT trunk's register (BASE + lag*4), never trunk 0's, and
* must not overwrite a hash that is already set. Compiles the REAL function
* (extracted verbatim from rtl837x_port.c) against register-recording shims. */
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#define __banked
#define __xdata
#define __code
/* register defs (mirror rtl837x_regs.h) */
#define RTL837X_TRK_MBR_CTRL_BASE 0x4f38
#define RTL837X_TRK_HASH_CTRL_BASE 0x4f48
#define LAG_HASH_SOURCE_PORT_NUMBER 0x01
#define LAG_HASH_L2_SMAC 0x02
#define LAG_HASH_L2_DMAC 0x04
#define LAG_HASH_L3_SIP 0x08
#define LAG_HASH_L3_DIP 0x10
#define LAG_HASH_L4_SPORT 0x20
#define LAG_HASH_L4_DPORT 0x40
#define LAG_HASH_DEFAULT (LAG_HASH_L2_SMAC|LAG_HASH_L2_DMAC|LAG_HASH_L3_SIP|LAG_HASH_L3_DIP|LAG_HASH_L4_SPORT|LAG_HASH_L4_DPORT)
#define LAG_HASH_RESET (LAG_HASH_SOURCE_PORT_NUMBER|LAG_HASH_L2_SMAC|LAG_HASH_L2_DMAC|LAG_HASH_L3_SIP|LAG_HASH_L3_DIP|LAG_HASH_L4_SPORT)
uint8_t SFR_DATA_24, SFR_DATA_16, SFR_DATA_8, SFR_DATA_0;
uint8_t sfr_data[4];
static uint32_t reg_store[0x10000];
void reg_read_m(uint16_t r){ uint32_t v=reg_store[r];
sfr_data[0]=(v>>24)&0xff; sfr_data[1]=(v>>16)&0xff; sfr_data[2]=(v>>8)&0xff; sfr_data[3]=v&0xff; }
void reg_write(uint16_t r){ reg_store[r]=((uint32_t)SFR_DATA_24<<24)|((uint32_t)SFR_DATA_16<<16)|((uint32_t)SFR_DATA_8<<8)|SFR_DATA_0; }
void print_string(char*s){(void)s;} void print_byte(uint8_t b){(void)b;}
void print_short(uint16_t s){(void)s;} void write_char(char c){(void)c;}
#define REG_SET(r,v) do{ SFR_DATA_24=((uint32_t)(v)>>24)&0xff; SFR_DATA_16=((uint32_t)(v)>>16)&0xff; \
SFR_DATA_8=((uint32_t)(v)>>8)&0xff; SFR_DATA_0=(v)&0xff; reg_write(r); }while(0)
#define REG_WRITE(r,a,b,c,d) do{ SFR_DATA_24=(a); SFR_DATA_16=(b); SFR_DATA_8=(c); SFR_DATA_0=(d); reg_write(r); }while(0)
/* THE REAL FUNCTION (extracted from rtl837x_port.c at build time) */
#include "port_lag_members_set.inc"
static int fails;
#define CK(cond,msg,...) do{ if(cond){printf("PASS " msg "\n",##__VA_ARGS__);} else {printf("FAIL " msg "\n",##__VA_ARGS__); fails++;} }while(0)
int main(void){
for(int lag=0; lag<4; lag++){
memset(reg_store,0,sizeof(reg_store));
port_lag_members_set(lag, 0x0180);
uint16_t h = RTL837X_TRK_HASH_CTRL_BASE + (lag<<2);
uint16_t m = RTL837X_TRK_MBR_CTRL_BASE + (lag<<2);
CK(reg_store[h]==LAG_HASH_DEFAULT, "lag%d: default 0x%02x -> hash reg 0x%04x", lag, LAG_HASH_DEFAULT, h);
CK(reg_store[m]==0x0180, "lag%d: members 0x0180 -> mbr reg 0x%04x", lag, m);
if(lag) CK(reg_store[RTL837X_TRK_HASH_CTRL_BASE]==0, "lag%d: trunk-0 hash NOT clobbered", lag);
}
/* guard: existing hash must survive a later member change */
memset(reg_store,0,sizeof(reg_store));
reg_store[RTL837X_TRK_HASH_CTRL_BASE+(1<<2)] = 0x06; /* lag1 already L2 */
port_lag_members_set(1, 0x00c0);
CK(reg_store[RTL837X_TRK_HASH_CTRL_BASE+(1<<2)]==0x06, "guard: preset lag1 hash 0x06 preserved");
/* the chip's reset value counts as unset and is replaced by the default */
memset(reg_store,0,sizeof(reg_store));
reg_store[RTL837X_TRK_HASH_CTRL_BASE+(2<<2)] = LAG_HASH_RESET;
port_lag_members_set(2, 0x0003);
CK(reg_store[RTL837X_TRK_HASH_CTRL_BASE+(2<<2)]==LAG_HASH_DEFAULT, "reset value 0x%02x replaced by default on lag2", LAG_HASH_RESET);
printf(fails? "\n=== FAIL (%d) ===\n" : "\n=== ALL PASS ===\n", fails);
return fails?1:0;
}
+1
View File
@@ -148,6 +148,7 @@ void send_mirror(void) { }
void send_mtu(void) { }
void send_config(void) { }
void send_cmd_log(void) { }
void send_lacp(void) { }
void send_lag(void) { }
void send_stp(void) { }
void send_stp_counters(void) { }