Merge branch 'fix-felix-dsa-taprio-gates-after-clock-jump'

Vladimir Oltean says:

====================
Fix Felix DSA taprio gates after clock jump

Richie Pearn presented a reproducible situation where traffic would get
blocked on the NXP LS1028A switch if a certain taprio schedule was
applied, and stepping the PTP clock would take place. The latter event
is an expected initial occurrence, but also at runtime, for example when
transitioning from one grandmaster to another.

The issue is completely described in patch 1/4, which also contains
the fix, but it has left me with some doubts regarding the need for
vsc9959_tas_clock_adjust() in general.

In order to prove to myself that vsc9959_tas_clock_adjust() is needed in
general, I have written a selftest for the tc-taprio data path in patch
4/4. On the LS1028A, we can clearly see the following failures without
that function:

INFO: Forcing a backward clock jump
TEST: ping                                                          [FAIL]
INFO: Setting up taprio after PTP
TEST: In band with gate                                             [FAIL]
        Reception of 100 packets failed
TEST: Out of band with gate                                         [FAIL]
        Reception of 100 packets failed

As for testing my fix from patch 1/4, that was quite a bit more complex
to do automatically. In fact, I couldn't find any other schedule that
would fail to be updated by vsc9959_tas_clock_adjust() as cleanly as
the schedule from Richie, so I've added that specific schedule as the
test_clock_jump_backward() test.

The test ordering is also (unfortunately) very strategic. Running the
selftest to the end dirties the GCL RAM, and when running
test_clock_jump_backward() once again, the GCL entries won't be all
zeroes as they were the first time around. They will contain bits and
pieces of old schedules, making it very challenging to make it fail.

Thus, test_clock_jump_backward() is the first in the test suite, and
without patch 1/4, it is only supposed to fail the _first_ time when
running after a clean boot.
====================

Link: https://patch.msgid.link/20250426144859.3128352-1-vladimir.oltean@nxp.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
Jakub Kicinski 2025-04-29 14:44:39 -07:00
commit 1e0bff3bb5
5 changed files with 454 additions and 7 deletions

View File

@ -1543,7 +1543,7 @@ static void vsc9959_tas_clock_adjust(struct ocelot *ocelot)
struct tc_taprio_qopt_offload *taprio;
struct ocelot_port *ocelot_port;
struct timespec64 base_ts;
int port;
int i, port;
u32 val;
mutex_lock(&ocelot->fwd_domain_lock);
@ -1575,6 +1575,9 @@ static void vsc9959_tas_clock_adjust(struct ocelot *ocelot)
QSYS_PARAM_CFG_REG_3_BASE_TIME_SEC_MSB_M,
QSYS_PARAM_CFG_REG_3);
for (i = 0; i < taprio->num_entries; i++)
vsc9959_tas_gcl_set(ocelot, i, &taprio->entries[i]);
ocelot_rmw(ocelot, QSYS_TAS_PARAM_CFG_CTRL_CONFIG_CHANGE,
QSYS_TAS_PARAM_CFG_CTRL_CONFIG_CHANGE,
QSYS_TAS_PARAM_CFG_CTRL);

View File

@ -0,0 +1 @@
run_net_forwarding_test.sh

View File

@ -266,18 +266,14 @@ run_test()
"${base_time}" \
"${CYCLE_TIME_NS}" \
"${SHIFT_TIME_NS}" \
"${GATE_DURATION_NS}" \
"${NUM_PKTS}" \
"${STREAM_VID}" \
"${STREAM_PRIO}" \
"" \
"${isochron_dat}"
# Count all received packets by looking at the non-zero RX timestamps
received=$(isochron report \
--input-file "${isochron_dat}" \
--printf-format "%u\n" --printf-args "R" | \
grep -w -v '0' | wc -l)
received=$(isochron_report_num_received "${isochron_dat}")
if [ "${received}" = "${expected}" ]; then
RET=0
else

View File

@ -0,0 +1,421 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
ALL_TESTS=" \
test_clock_jump_backward \
test_taprio_after_ptp \
test_max_sdu \
test_clock_jump_backward_forward \
"
NUM_NETIFS=4
source tc_common.sh
source lib.sh
source tsn_lib.sh
require_command python3
# The test assumes the usual topology from the README, where h1 is connected to
# swp1, h2 to swp2, and swp1 and swp2 are together in a bridge.
# Additional assumption: h1 and h2 use the same PHC, and so do swp1 and swp2.
# By synchronizing h1 to swp1 via PTP, h2 is also implicitly synchronized to
# swp1 (and both to CLOCK_REALTIME).
h1=${NETIFS[p1]}
swp1=${NETIFS[p2]}
swp2=${NETIFS[p3]}
h2=${NETIFS[p4]}
UDS_ADDRESS_H1="/var/run/ptp4l_h1"
UDS_ADDRESS_SWP1="/var/run/ptp4l_swp1"
H1_IPV4="192.0.2.1"
H2_IPV4="192.0.2.2"
H1_IPV6="2001:db8:1::1"
H2_IPV6="2001:db8:1::2"
# Tunables
NUM_PKTS=100
STREAM_VID=10
STREAM_PRIO_1=6
STREAM_PRIO_2=5
STREAM_PRIO_3=4
# PTP uses TC 0
ALL_GATES=$((1 << 0 | 1 << STREAM_PRIO_1 | 1 << STREAM_PRIO_2))
# Use a conservative cycle of 10 ms to allow the test to still pass when the
# kernel has some extra overhead like lockdep etc
CYCLE_TIME_NS=10000000
# Create two Gate Control List entries, one OPEN and one CLOSE, of equal
# durations
GATE_DURATION_NS=$((CYCLE_TIME_NS / 2))
# Give 2/3 of the cycle time to user space and 1/3 to the kernel
FUDGE_FACTOR=$((CYCLE_TIME_NS / 3))
# Shift the isochron base time by half the gate time, so that packets are
# always received by swp1 close to the middle of the time slot, to minimize
# inaccuracies due to network sync
SHIFT_TIME_NS=$((GATE_DURATION_NS / 2))
path_delay=
h1_create()
{
simple_if_init $h1 $H1_IPV4/24 $H1_IPV6/64
}
h1_destroy()
{
simple_if_fini $h1 $H1_IPV4/24 $H1_IPV6/64
}
h2_create()
{
simple_if_init $h2 $H2_IPV4/24 $H2_IPV6/64
}
h2_destroy()
{
simple_if_fini $h2 $H2_IPV4/24 $H2_IPV6/64
}
switch_create()
{
local h2_mac_addr=$(mac_get $h2)
ip link set $swp1 up
ip link set $swp2 up
ip link add br0 type bridge vlan_filtering 1
ip link set $swp1 master br0
ip link set $swp2 master br0
ip link set br0 up
bridge vlan add dev $swp2 vid $STREAM_VID
bridge vlan add dev $swp1 vid $STREAM_VID
bridge fdb add dev $swp2 \
$h2_mac_addr vlan $STREAM_VID static master
}
switch_destroy()
{
ip link del br0
}
ptp_setup()
{
# Set up swp1 as a master PHC for h1, synchronized to the local
# CLOCK_REALTIME.
phc2sys_start $UDS_ADDRESS_SWP1
ptp4l_start $h1 true $UDS_ADDRESS_H1
ptp4l_start $swp1 false $UDS_ADDRESS_SWP1
}
ptp_cleanup()
{
ptp4l_stop $swp1
ptp4l_stop $h1
phc2sys_stop
}
txtime_setup()
{
local if_name=$1
tc qdisc add dev $if_name clsact
# Classify PTP on TC 7 and isochron on TC 6
tc filter add dev $if_name egress protocol 0x88f7 \
flower action skbedit priority 7
tc filter add dev $if_name egress protocol 802.1Q \
flower vlan_ethtype 0xdead action skbedit priority 6
tc qdisc add dev $if_name handle 100: parent root mqprio num_tc 8 \
queues 1@0 1@1 1@2 1@3 1@4 1@5 1@6 1@7 \
map 0 1 2 3 4 5 6 7 \
hw 1
# Set up TC 5, 6, 7 for SO_TXTIME. tc-mqprio queues count from 1.
tc qdisc replace dev $if_name parent 100:$((STREAM_PRIO_1 + 1)) etf \
clockid CLOCK_TAI offload delta $FUDGE_FACTOR
tc qdisc replace dev $if_name parent 100:$((STREAM_PRIO_2 + 1)) etf \
clockid CLOCK_TAI offload delta $FUDGE_FACTOR
tc qdisc replace dev $if_name parent 100:$((STREAM_PRIO_3 + 1)) etf \
clockid CLOCK_TAI offload delta $FUDGE_FACTOR
}
txtime_cleanup()
{
local if_name=$1
tc qdisc del dev $if_name clsact
tc qdisc del dev $if_name root
}
taprio_replace()
{
local if_name="$1"; shift
local extra_args="$1"; shift
# STREAM_PRIO_1 always has an open gate.
# STREAM_PRIO_2 has a gate open for GATE_DURATION_NS (half the cycle time)
# STREAM_PRIO_3 always has a closed gate.
tc qdisc replace dev $if_name root stab overhead 24 taprio num_tc 8 \
queues 1@0 1@1 1@2 1@3 1@4 1@5 1@6 1@7 \
map 0 1 2 3 4 5 6 7 \
sched-entry S $(printf "%x" $ALL_GATES) $GATE_DURATION_NS \
sched-entry S $(printf "%x" $((ALL_GATES & ~(1 << STREAM_PRIO_2)))) $GATE_DURATION_NS \
base-time 0 flags 0x2 $extra_args
taprio_wait_for_admin $if_name
}
taprio_cleanup()
{
local if_name=$1
tc qdisc del dev $if_name root
}
probe_path_delay()
{
local isochron_dat="$(mktemp)"
local received
log_info "Probing path delay"
isochron_do "$h1" "$h2" "$UDS_ADDRESS_H1" "" 0 \
"$CYCLE_TIME_NS" "" "" "$NUM_PKTS" \
"$STREAM_VID" "$STREAM_PRIO_1" "" "$isochron_dat"
received=$(isochron_report_num_received "$isochron_dat")
if [ "$received" != "$NUM_PKTS" ]; then
echo "Cannot establish basic data path between $h1 and $h2"
exit $ksft_fail
fi
printf "pdelay = {}\n" > isochron_data.py
isochron report --input-file "$isochron_dat" \
--printf-format "pdelay[%u] = %d - %d\n" \
--printf-args "qRT" \
>> isochron_data.py
cat <<-'EOF' > isochron_postprocess.py
#!/usr/bin/env python3
from isochron_data import pdelay
import numpy as np
w = np.array(list(pdelay.values()))
print("{}".format(np.max(w)))
EOF
path_delay=$(python3 ./isochron_postprocess.py)
log_info "Path delay from $h1 to $h2 estimated at $path_delay ns"
if [ "$path_delay" -gt "$GATE_DURATION_NS" ]; then
echo "Path delay larger than gate duration, aborting"
exit $ksft_fail
fi
rm -f ./isochron_data.py 2> /dev/null
rm -f ./isochron_postprocess.py 2> /dev/null
rm -f "$isochron_dat" 2> /dev/null
}
setup_prepare()
{
vrf_prepare
h1_create
h2_create
switch_create
txtime_setup $h1
# Temporarily set up PTP just to probe the end-to-end path delay.
ptp_setup
probe_path_delay
ptp_cleanup
}
cleanup()
{
pre_cleanup
isochron_recv_stop
txtime_cleanup $h1
switch_destroy
h2_destroy
h1_destroy
vrf_cleanup
}
run_test()
{
local base_time=$1; shift
local stream_prio=$1; shift
local expected_delay=$1; shift
local should_fail=$1; shift
local test_name=$1; shift
local isochron_dat="$(mktemp)"
local received
local median_delay
RET=0
# Set the shift time equal to the cycle time, which effectively
# cancels the default advance time. Packets won't be sent early in
# software, which ensures that they won't prematurely enter through
# the open gate in __test_out_of_band(). Also, the gate is open for
# long enough that this won't cause a problem in __test_in_band().
isochron_do "$h1" "$h2" "$UDS_ADDRESS_H1" "" "$base_time" \
"$CYCLE_TIME_NS" "$SHIFT_TIME_NS" "$GATE_DURATION_NS" \
"$NUM_PKTS" "$STREAM_VID" "$stream_prio" "" "$isochron_dat"
received=$(isochron_report_num_received "$isochron_dat")
[ "$received" = "$NUM_PKTS" ]
check_err_fail $should_fail $? "Reception of $NUM_PKTS packets"
if [ $should_fail = 0 ] && [ "$received" = "$NUM_PKTS" ]; then
printf "pdelay = {}\n" > isochron_data.py
isochron report --input-file "$isochron_dat" \
--printf-format "pdelay[%u] = %d - %d\n" \
--printf-args "qRT" \
>> isochron_data.py
cat <<-'EOF' > isochron_postprocess.py
#!/usr/bin/env python3
from isochron_data import pdelay
import numpy as np
w = np.array(list(pdelay.values()))
print("{}".format(int(np.median(w))))
EOF
median_delay=$(python3 ./isochron_postprocess.py)
# If the condition below is true, packets were delayed by a closed gate
[ "$median_delay" -gt $((path_delay + expected_delay)) ]
check_fail $? "Median delay $median_delay is greater than expected delay $expected_delay plus path delay $path_delay"
# If the condition below is true, packets were sent expecting them to
# hit a closed gate in the switch, but were not delayed
[ "$expected_delay" -gt 0 ] && [ "$median_delay" -lt "$expected_delay" ]
check_fail $? "Median delay $median_delay is less than expected delay $expected_delay"
fi
log_test "$test_name"
rm -f ./isochron_data.py 2> /dev/null
rm -f ./isochron_postprocess.py 2> /dev/null
rm -f "$isochron_dat" 2> /dev/null
}
__test_always_open()
{
run_test 0.000000000 $STREAM_PRIO_1 0 0 "Gate always open"
}
__test_always_closed()
{
run_test 0.000000000 $STREAM_PRIO_3 0 1 "Gate always closed"
}
__test_in_band()
{
# Send packets in-band with the OPEN gate entry
run_test 0.000000000 $STREAM_PRIO_2 0 0 "In band with gate"
}
__test_out_of_band()
{
# Send packets in-band with the CLOSE gate entry
run_test 0.005000000 $STREAM_PRIO_2 \
$((GATE_DURATION_NS - SHIFT_TIME_NS)) 0 \
"Out of band with gate"
}
run_subtests()
{
__test_always_open
__test_always_closed
__test_in_band
__test_out_of_band
}
test_taprio_after_ptp()
{
log_info "Setting up taprio after PTP"
ptp_setup
taprio_replace $swp2
run_subtests
taprio_cleanup $swp2
ptp_cleanup
}
__test_under_max_sdu()
{
# Limit max-sdu for STREAM_PRIO_1
taprio_replace "$swp2" "max-sdu 0 0 0 0 0 0 100 0"
run_test 0.000000000 $STREAM_PRIO_1 0 0 "Under maximum SDU"
}
__test_over_max_sdu()
{
# Limit max-sdu for STREAM_PRIO_1
taprio_replace "$swp2" "max-sdu 0 0 0 0 0 0 20 0"
run_test 0.000000000 $STREAM_PRIO_1 0 1 "Over maximum SDU"
}
test_max_sdu()
{
ptp_setup
__test_under_max_sdu
__test_over_max_sdu
taprio_cleanup $swp2
ptp_cleanup
}
# Perform a clock jump in the past without synchronization running, so that the
# time base remains where it was set by phc_ctl.
test_clock_jump_backward()
{
# This is a more complex schedule specifically crafted in a way that
# has been problematic on NXP LS1028A. Not much to test with it other
# than the fact that it passes traffic.
tc qdisc replace dev $swp2 root stab overhead 24 taprio num_tc 8 \
queues 1@0 1@1 1@2 1@3 1@4 1@5 1@6 1@7 map 0 1 2 3 4 5 6 7 \
base-time 0 sched-entry S 20 300000 sched-entry S 10 200000 \
sched-entry S 20 300000 sched-entry S 48 200000 \
sched-entry S 20 300000 sched-entry S 83 200000 \
sched-entry S 40 300000 sched-entry S 00 200000 flags 2
log_info "Forcing a backward clock jump"
phc_ctl $swp1 set 0
ping_test $h1 192.0.2.2
taprio_cleanup $swp2
}
# Test that taprio tolerates clock jumps.
# Since ptp4l and phc2sys are running, it is expected for the time to
# eventually recover (through yet another clock jump). Isochron waits
# until that is the case.
test_clock_jump_backward_forward()
{
log_info "Forcing a backward and a forward clock jump"
taprio_replace $swp2
phc_ctl $swp1 set 0
ptp_setup
ping_test $h1 192.0.2.2
run_subtests
ptp_cleanup
taprio_cleanup $swp2
}
tc_offload_check
if [[ $? -ne 0 ]]; then
log_test_skip "Could not test offloaded functionality"
exit $EXIT_STATUS
fi
trap cleanup EXIT
setup_prepare
setup_wait
tests_run
exit $EXIT_STATUS

View File

@ -2,6 +2,8 @@
# SPDX-License-Identifier: GPL-2.0
# Copyright 2021-2022 NXP
tc_testing_scripts_dir=$(dirname $0)/../../tc-testing/scripts
REQUIRE_ISOCHRON=${REQUIRE_ISOCHRON:=yes}
REQUIRE_LINUXPTP=${REQUIRE_LINUXPTP:=yes}
@ -18,6 +20,7 @@ fi
if [[ "$REQUIRE_LINUXPTP" = "yes" ]]; then
require_command phc2sys
require_command ptp4l
require_command phc_ctl
fi
phc2sys_start()
@ -182,6 +185,7 @@ isochron_do()
local base_time=$1; shift
local cycle_time=$1; shift
local shift_time=$1; shift
local window_size=$1; shift
local num_pkts=$1; shift
local vid=$1; shift
local priority=$1; shift
@ -212,6 +216,10 @@ isochron_do()
extra_args="${extra_args} --shift-time=${shift_time}"
fi
if ! [ -z "${window_size}" ]; then
extra_args="${extra_args} --window-size=${window_size}"
fi
if [ "${use_l2}" = "true" ]; then
extra_args="${extra_args} --l2 --etype=0xdead ${vid}"
receiver_extra_args="--l2 --etype=0xdead"
@ -247,3 +255,21 @@ isochron_do()
cpufreq_restore ${ISOCHRON_CPU}
}
isochron_report_num_received()
{
local isochron_dat=$1; shift
# Count all received packets by looking at the non-zero RX timestamps
isochron report \
--input-file "${isochron_dat}" \
--printf-format "%u\n" --printf-args "R" | \
grep -w -v '0' | wc -l
}
taprio_wait_for_admin()
{
local if_name="$1"; shift
"$tc_testing_scripts_dir/taprio_wait_for_admin.sh" "$(which tc)" "$if_name"
}