Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 107 additions & 28 deletions can/bit_timing.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,31 @@ def __init__(
"sjw": sjw,
"nof_samples": nof_samples,
}
# Retain fallback limits for clock recreation; validation uses its arguments.
self._tseg1_max = max(16, tseg1)
self._tseg2_max = max(8, tseg2)
self._brp_max = max(64, brp)
if strict:
self._validate()
self._restrict_to_minimum_range()

def _validate(self) -> None:
if not 1 <= self.brp <= 64:
raise ValueError(f"bitrate prescaler (={self.brp}) must be in [1...64].")
def _validate(
self,
*,
tseg1_max: int = 16,
tseg2_max: int = 8,
brp_max: int = 64,
) -> None:
if not 1 <= self.brp <= brp_max:
raise ValueError(
f"bitrate prescaler (={self.brp}) must be in [1...{brp_max}]."
)

if not 1 <= self.tseg1 <= 16:
raise ValueError(f"tseg1 (={self.tseg1}) must be in [1...16].")
if not 1 <= self.tseg1 <= tseg1_max:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can this ever fail if you set self._tseg1_max = max(16, tseg1) in the init method? Am i missing something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tseg1_max in this check is the _validate() argument (default 16), rather than self._tseg1_max. The stored attributes retain limits for clock recreation; the sample-point factory replaces those fallback values with the caller's explicit controller limits. For example, constructing with tseg1=17, strict=True still raises ValueError, even though the stored fallback maximum is 17. I've added a regression for that case and clarified the comment.

While checking this, I found a separate gap in the clock-recreation shortcut: a timing created at 8 MHz / 500 kbit/s with brp_max=1 could be recreated at 16 MHz with brp=2. In 33a212a, the shortcut validates against the retained controller limits before returning, then falls back to the bounded solver when necessary. The new regression rejects that case and accepts the brp_max=2 boundary control.

Local validation on Python 3.14.10: 36 timing/util tests passed; configured lint and all five mypy targets (3.11–3.15) passed; Sphinx HTML and 39 doctests passed. Black used one worker for this environment. The three intentional internal-access annotations follow the existing convention in this file. Hosted CI is running on the new revision.

Timothy Anderson — Spotted Ridge Engineering / OC Labs · GDN (Global Decision Network)
timothyanderson096@gmail.com

raise ValueError(f"tseg1 (={self.tseg1}) must be in [1...{tseg1_max}].")

if not 1 <= self.tseg2 <= 8:
raise ValueError(f"tseg2 (={self.tseg2}) must be in [1...8].")
if not 1 <= self.tseg2 <= tseg2_max:
raise ValueError(f"tseg2 (={self.tseg2}) must be in [1...{tseg2_max}].")

if not 1 <= self.sjw <= 4:
raise ValueError(f"sjw (={self.sjw}) must be in [1...4].")
Expand Down Expand Up @@ -215,7 +227,14 @@ def from_registers(

@classmethod
def iterate_from_sample_point(
cls, f_clock: int, bitrate: int, sample_point: float = 69.0
cls,
f_clock: int,
bitrate: int,
sample_point: float = 69.0,
*,
tseg1_max: int = 16,
tseg2_max: int = 8,
brp_max: int = 64,
) -> Iterator["BitTiming"]:
"""Create a :class:`~can.BitTiming` iterator with all the solutions for a sample point.

Expand All @@ -225,45 +244,74 @@ def iterate_from_sample_point(
Bitrate in bit/s.
:param int sample_point:
The sample point value in percent.
:param int tseg1_max:
Maximum time segment 1 value supported by the CAN controller.
:param int tseg2_max:
Maximum time segment 2 value supported by the CAN controller.
:param int brp_max:
Maximum bit rate prescaler supported by the CAN controller.
:raises ValueError:
if the arguments are invalid.
"""

if sample_point < 50.0:
raise ValueError(f"sample_point (={sample_point}) must not be below 50%.")

for brp in range(1, 65):
for name, value in (
("tseg1_max", tseg1_max),
("tseg2_max", tseg2_max),
("brp_max", brp_max),
):
if value < 1:
raise ValueError(f"{name} (={value}) must be at least 1.")

if not 5_000 <= bitrate <= 1_000_000:
return
Comment thread
timothyanderson096-ocdealcheck marked this conversation as resolved.
Resolved

for brp in range(1, brp_max + 1):
nbt = int(f_clock / (bitrate * brp))
if nbt < 8:
break

effective_bitrate = f_clock / (nbt * brp)
if abs(effective_bitrate - bitrate) > bitrate / 256:
continue
if not 5_000 <= round(effective_bitrate) <= 1_000_000:
continue

tseg1_min = max(1, nbt - tseg2_max - 1)
tseg1_max_feasible = min(tseg1_max, nbt - 2)
if tseg1_min > tseg1_max_feasible:
continue

tseg1 = round(sample_point / 100 * nbt) - 1
# limit tseg1, so tseg2 is at least 1 TQ
tseg1 = min(tseg1, nbt - 2)
tseg1 = max(tseg1_min, min(tseg1, tseg1_max_feasible))

tseg2 = nbt - tseg1 - 1
sjw = min(tseg2, 4)

try:
bt = BitTiming(
f_clock=f_clock,
brp=brp,
tseg1=tseg1,
tseg2=tseg2,
sjw=sjw,
strict=True,
)
yield bt
except ValueError:
continue
timing = cls(
f_clock=f_clock,
brp=brp,
tseg1=tseg1,
tseg2=tseg2,
sjw=sjw,
)
timing._tseg1_max = tseg1_max
timing._tseg2_max = tseg2_max
timing._brp_max = brp_max
yield timing

@classmethod
def from_sample_point(
cls, f_clock: int, bitrate: int, sample_point: float = 69.0
cls,
f_clock: int,
bitrate: int,
sample_point: float = 69.0,
*,
tseg1_max: int = 16,
tseg2_max: int = 8,
brp_max: int = 64,
) -> "BitTiming":
"""Create a :class:`~can.BitTiming` instance for a sample point.

Expand All @@ -280,6 +328,12 @@ def from_sample_point(
Bitrate in bit/s.
:param int sample_point:
The sample point value in percent.
:param int tseg1_max:
Maximum time segment 1 value supported by the CAN controller.
:param int tseg2_max:
Maximum time segment 2 value supported by the CAN controller.
:param int brp_max:
Maximum bit rate prescaler supported by the CAN controller.
:raises ValueError:
if the arguments are invalid.
"""
Expand All @@ -288,7 +342,14 @@ def from_sample_point(
raise ValueError(f"sample_point (={sample_point}) must not be below 50%.")

possible_solutions: list[BitTiming] = list(
cls.iterate_from_sample_point(f_clock, bitrate, sample_point)
cls.iterate_from_sample_point(
f_clock,
bitrate,
sample_point,
tseg1_max=tseg1_max,
tseg2_max=tseg2_max,
brp_max=brp_max,
)
)

if not possible_solutions:
Expand Down Expand Up @@ -413,7 +474,7 @@ def recreate_with_f_clock(self, f_clock: int) -> "BitTiming":
"""
# try the most simple solution first: another bitrate prescaler
try:
return BitTiming.from_bitrate_and_segments(
bt = BitTiming.from_bitrate_and_segments(
f_clock=f_clock,
bitrate=self.bitrate,
tseg1=self.tseg1,
Expand All @@ -422,12 +483,26 @@ def recreate_with_f_clock(self, f_clock: int) -> "BitTiming":
nof_samples=self.nof_samples,
strict=True,
)
bt._validate( # pylint: disable=protected-access
tseg1_max=self._tseg1_max,
tseg2_max=self._tseg2_max,
brp_max=self._brp_max,
)
bt._tseg1_max = self._tseg1_max # pylint: disable=protected-access
bt._tseg2_max = self._tseg2_max # pylint: disable=protected-access
bt._brp_max = self._brp_max # pylint: disable=protected-access
return bt
except ValueError:
pass

# create a new timing instance with the same sample point
bt = BitTiming.from_sample_point(
f_clock=f_clock, bitrate=self.bitrate, sample_point=self.sample_point
f_clock=f_clock,
bitrate=self.bitrate,
sample_point=self.sample_point,
tseg1_max=self._tseg1_max,
tseg2_max=self._tseg2_max,
brp_max=self._brp_max,
)
if abs(bt.sample_point - self.sample_point) > 1.0:
raise ValueError(
Expand All @@ -438,7 +513,11 @@ def recreate_with_f_clock(self, f_clock: int) -> "BitTiming":
sjw = max(1, min(4, bt.tseg2, sjw))
bt._data["sjw"] = sjw # pylint: disable=protected-access
bt._data["nof_samples"] = self.nof_samples # pylint: disable=protected-access
bt._validate() # pylint: disable=protected-access
bt._validate( # pylint: disable=protected-access
tseg1_max=self._tseg1_max,
tseg2_max=self._tseg2_max,
brp_max=self._brp_max,
)
return bt

def __str__(self) -> str:
Expand Down
14 changes: 14 additions & 0 deletions doc/bit_timing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@ produce an overview of possible bit timings for your desired bit rate:
BR: 250_000 bit/s, SP: 87.50%, BRP: 2, TSEG1: 13, TSEG2: 2, SJW: 2, BTR: 411Ch, CLK: 8MHz
BR: 250_000 bit/s, SP: 93.75%, BRP: 2, TSEG1: 14, TSEG2: 1, SJW: 1, BTR: 010Dh, CLK: 8MHz

Controller-specific maximum values can be supplied when the standard timing
limits are too restrictive:

.. code-block:: python

timing = can.BitTiming.from_sample_point(
f_clock=160_000_000,
bitrate=250_000,
sample_point=87.5,
tseg1_max=256,
tseg2_max=128,
brp_max=512,
)


It is possible to specify CAN 2.0 bit timings
using the config file:
Expand Down
2 changes: 2 additions & 0 deletions doc/changelog.d/2083.fixed.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Allow ``BitTiming.from_sample_point`` to find valid timings for CAN controllers with bit rate prescalers above 32.
Preserve controller limits when recreating timings at a different clock frequency.
126 changes: 126 additions & 0 deletions test/test_bit_timing.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,113 @@ def test_from_sample_point():
)


def test_from_sample_point_with_extended_hardware_limits():
timing = can.BitTiming.from_sample_point(
f_clock=160_000_000,
bitrate=250_000,
sample_point=87.5,
)

assert timing.bitrate == 250_000
assert timing.sample_point == 87.5
assert timing.brp == 40
assert timing.tseg1 == 13
assert timing.tseg2 == 2

extended_timing = can.BitTiming.from_sample_point(
f_clock=160_000_000,
bitrate=250_000,
sample_point=87.5,
tseg1_max=256,
tseg2_max=128,
brp_max=512,
)
assert extended_timing.brp == 4
assert extended_timing.tseg1 == 139
assert extended_timing.tseg2 == 20

with pytest.raises(ValueError, match="No suitable bit timings found"):
can.BitTiming.from_sample_point(
f_clock=160_000_000,
bitrate=250_000,
sample_point=87.5,
brp_max=28,
)

with pytest.raises(ValueError, match="No suitable bit timings found"):
can.BitTiming.from_sample_point(
f_clock=80_000_000,
bitrate=2_000_000,
sample_point=75.0,
)

for parameter in ("tseg1_max", "tseg2_max", "brp_max"):
with pytest.raises(ValueError, match=rf"{parameter} \(=0\) must be at least 1"):
list(
can.BitTiming.iterate_from_sample_point(
f_clock=16_000_000,
bitrate=500_000,
**{parameter: 0},
)
)


def test_from_sample_point_rejects_effective_bitrate_outside_supported_range():
with pytest.raises(ValueError, match="No suitable bit timings found"):
can.BitTiming.from_sample_point(
f_clock=8_024_000,
bitrate=1_000_000,
sample_point=75.0,
)


def test_strict_timing_rejects_segment_above_standard_limit():
with pytest.raises(ValueError, match=r"tseg1 \(=17\) must be in \[1\.\.\.16\]"):
can.BitTiming(f_clock=8_000_000, brp=1, tseg1=17, tseg2=2, sjw=2, strict=True)


@pytest.mark.parametrize("brp_max", [1, 2])
def test_recreate_with_f_clock_respects_controller_prescaler_limit(brp_max):
timing = can.BitTiming.from_sample_point(
f_clock=8_000_000,
bitrate=500_000,
sample_point=75.0,
brp_max=brp_max,
)
assert timing.brp == 1

if brp_max == 1:
with pytest.raises(ValueError, match="No suitable bit timings found"):
timing.recreate_with_f_clock(16_000_000)
else:
recreated = timing.recreate_with_f_clock(16_000_000)
assert recreated.brp == 2
assert recreated.bitrate == 500_000
assert recreated.sample_point == 75.0


@pytest.mark.parametrize(
("limits", "expected_tseg1", "expected_tseg2"),
[
({"tseg1_max": 4}, 4, 3),
({"tseg2_max": 1}, 6, 1),
],
)
def test_from_sample_point_clamps_to_controller_segment_limits(
limits, expected_tseg1, expected_tseg2
):
timing = can.BitTiming.from_sample_point(
f_clock=8_000_000,
bitrate=1_000_000,
sample_point=75.0,
**limits,
)

assert timing.bitrate == 1_000_000
assert timing.tseg1 == expected_tseg1
assert timing.tseg2 == expected_tseg2


def test_iterate_from_sample_point():
for sp in range(50, 100):
solutions = list(
Expand Down Expand Up @@ -492,6 +599,25 @@ def test_recreate_with_f_clock():
)
assert timing_8mhz.nof_samples == timing_16mhz.nof_samples

extended_timing_160mhz = can.BitTiming.from_sample_point(
f_clock=160_000_000,
bitrate=5_000,
sample_point=75.0,
tseg1_max=256,
tseg2_max=128,
brp_max=512,
)
extended_timing_200mhz = extended_timing_160mhz.recreate_with_f_clock(
f_clock=200_000_000
)
assert (
abs(extended_timing_200mhz.bitrate - extended_timing_160mhz.bitrate)
<= extended_timing_160mhz.bitrate / 256
)
assert extended_timing_200mhz.sample_point == pytest.approx(
extended_timing_160mhz.sample_point, abs=1.0
)

timing_16mhz = can.BitTiming(
f_clock=16000000, brp=2, tseg1=12, tseg2=3, sjw=3, nof_samples=1
)
Expand Down
Loading